dsh-diff-approval 0.5.0 → 0.7.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
@@ -108,6 +108,28 @@ var PendingDiffStore = class {
108
108
  for (const [key, indexedId] of this.pathIndex) if (indexedId === id) this.pathIndex.delete(key);
109
109
  return true;
110
110
  }
111
+ /**
112
+ * Advance one entry's tracked content after a block-level keep/revert. The
113
+ * entry keeps its id and path; only the given side's text and capture time
114
+ * move. When the caller decides the sides now match, it removes the entry
115
+ * instead of updating it.
116
+ * @param sessionId - the owning session.
117
+ * @param id - the entry id.
118
+ * @param patch - the side to advance (`oldText` for keep, `newText` for revert).
119
+ * @returns whether the entry changed.
120
+ */
121
+ update(sessionId, id, patch) {
122
+ const key = entryKey(sessionId, id);
123
+ const entry = this.entries.get(key);
124
+ if (entry === void 0) return false;
125
+ const next = {
126
+ ...entry,
127
+ ...patch,
128
+ updatedAt: Date.now()
129
+ };
130
+ this.entries.set(key, next);
131
+ return true;
132
+ }
111
133
  /** Total entry count across all sessions. */
112
134
  get size() {
113
135
  return this.entries.size;
@@ -760,7 +782,10 @@ function apply(ctx, config) {
760
782
  if (sessionId === void 0) return rpcError("sessionId must be a non-empty string");
761
783
  return {
762
784
  ok: true,
763
- value: { files: await listWithState(await workspaceEntries(sessionId)) }
785
+ value: {
786
+ files: await listWithState(await workspaceEntries(sessionId)),
787
+ workspacePath: workspaceOf(sessionId)?.path
788
+ }
764
789
  };
765
790
  }
766
791
  case "keep": {
@@ -797,6 +822,51 @@ function apply(ctx, config) {
797
822
  value: { outcome: "reverted" }
798
823
  };
799
824
  }
825
+ case "block-keep": {
826
+ const blockTarget = blockTargetOf(payload);
827
+ if (blockTarget === void 0) return rpcError("sessionId, id, and block must be valid");
828
+ await ensureLoaded(blockTarget.sessionId);
829
+ const entry = store.get(blockTarget.sessionId, blockTarget.id);
830
+ if (entry === void 0) return {
831
+ ok: true,
832
+ value: { outcome: "missing" }
833
+ };
834
+ const accepted = contentRangeOf(entry.newText, blockTarget.block.newStart, blockTarget.block.newEnd);
835
+ const updatedOld = replaceContentLines(entry.oldText, blockTarget.block.oldStart, blockTarget.block.oldEnd, accepted);
836
+ if (updatedOld === entry.newText) store.remove(blockTarget.sessionId, blockTarget.id);
837
+ else store.update(blockTarget.sessionId, blockTarget.id, { oldText: updatedOld });
838
+ await persistSession(blockTarget.sessionId);
839
+ return {
840
+ ok: true,
841
+ value: { outcome: "kept" }
842
+ };
843
+ }
844
+ case "block-revert": {
845
+ const blockTarget = blockTargetOf(payload);
846
+ if (blockTarget === void 0) return rpcError("sessionId, id, and block must be valid");
847
+ await ensureLoaded(blockTarget.sessionId);
848
+ const entry = store.get(blockTarget.sessionId, blockTarget.id);
849
+ if (entry === void 0) return {
850
+ ok: true,
851
+ value: { outcome: "missing" }
852
+ };
853
+ const restored = contentRangeOf(entry.oldText, blockTarget.block.oldStart, blockTarget.block.oldEnd);
854
+ const updatedNew = replaceContentLines(entry.newText, blockTarget.block.newStart, blockTarget.block.newEnd, restored);
855
+ try {
856
+ const resolved = await ctx.fs.resolve(entry.path, { signal });
857
+ if (entry.kind === "create" && updatedNew === "") await rm(ctx.fs.processPath(resolved), { force: true });
858
+ else await ctx.fs.writeText(resolved, updatedNew, void 0, signal);
859
+ } catch (error) {
860
+ return rpcError(`block revert failed: ${errorMessage(error)}`);
861
+ }
862
+ if (updatedNew === entry.oldText) store.remove(blockTarget.sessionId, blockTarget.id);
863
+ else store.update(blockTarget.sessionId, blockTarget.id, { newText: updatedNew });
864
+ await persistSession(blockTarget.sessionId);
865
+ return {
866
+ ok: true,
867
+ value: { outcome: "reverted" }
868
+ };
869
+ }
800
870
  case "open": {
801
871
  const target = openTargetOf(payload);
802
872
  if (target === void 0) return rpcError("sessionId, id, and action must be valid");
@@ -836,6 +906,73 @@ function sessionOf(payload) {
836
906
  const value = payload.sessionId;
837
907
  return typeof value === "string" && value.length > 0 ? SessionId(value) : void 0;
838
908
  }
909
+ /** The content lines of `text`, matching the diff's line numbering (a single
910
+ trailing newline is a terminator, not an extra empty line). */
911
+ function contentLinesOf(text) {
912
+ if (text === "") return [];
913
+ const lines = text.split("\n");
914
+ if (lines[lines.length - 1] === "") lines.pop();
915
+ return lines;
916
+ }
917
+ /** Rebuild text from content lines, keeping `original`'s trailing-newline convention. */
918
+ function fromContentLines(original, lines) {
919
+ if (lines.length === 0) return "";
920
+ return lines.join("\n") + (original.endsWith("\n") ? "\n" : "");
921
+ }
922
+ /** The content lines [start..end] (1-based inclusive) of `text`; empty when start > end. */
923
+ function contentRangeOf(text, start, end) {
924
+ if (start > end) return [];
925
+ return contentLinesOf(text).slice(start - 1, end);
926
+ }
927
+ /**
928
+ * Replace the content lines [start..end] (1-based) of `text` with `replacement`
929
+ * lines. An empty range (`start > end`) inserts before line `start`. Out-of-range
930
+ * bounds clamp; the trailing-newline convention of `text` is preserved.
931
+ */
932
+ function replaceContentLines(text, start, end, replacement) {
933
+ const lines = contentLinesOf(text);
934
+ const count = lines.length;
935
+ if (start > end) {
936
+ const at = Math.min(Math.max(start, 1), count + 1);
937
+ return fromContentLines(text, [
938
+ ...lines.slice(0, at - 1),
939
+ ...replacement,
940
+ ...lines.slice(at - 1)
941
+ ]);
942
+ }
943
+ const s = Math.min(Math.max(start, 1), count + 1);
944
+ const e = Math.min(Math.max(end, 1), count);
945
+ if (s > e) return text;
946
+ return fromContentLines(text, [
947
+ ...lines.slice(0, s - 1),
948
+ ...replacement,
949
+ ...lines.slice(e)
950
+ ]);
951
+ }
952
+ /** Narrow a wire payload to one block keep/revert target. */
953
+ function blockTargetOf(payload) {
954
+ const target = targetOf(payload);
955
+ if (target === void 0) return void 0;
956
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return void 0;
957
+ const block = payload.block;
958
+ if (typeof block !== "object" || block === null || Array.isArray(block)) return void 0;
959
+ const { oldStart, oldEnd, newStart, newEnd } = block;
960
+ if (![
961
+ oldStart,
962
+ oldEnd,
963
+ newStart,
964
+ newEnd
965
+ ].every((value) => typeof value === "number" && Number.isFinite(value))) return void 0;
966
+ return {
967
+ ...target,
968
+ block: {
969
+ oldStart,
970
+ oldEnd,
971
+ newStart,
972
+ newEnd
973
+ }
974
+ };
975
+ }
839
976
  /** Narrow a wire payload to one keep/revert target. */
840
977
  function targetOf(payload) {
841
978
  const sessionId = sessionOf(payload);
@@ -4,4 +4,4 @@ import type { PendingPanelFace } from './slots.ts';
4
4
  /** Full panel props composed by the sidebar footer-action slot. */
5
5
  export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<PendingPanelFace> & PropsLocale<'diff-approval'>;
6
6
  /** Render the pending-edit review panel and its unified footer action. */
7
- export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onOpen, t, }: PendingPanelProps): import("react").JSX.Element;
7
+ export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, t, }: PendingPanelProps): import("react").JSX.Element;
@@ -10,7 +10,9 @@ export declare const zh: {
10
10
  'panel.group.others': string;
11
11
  'panel.aria': string;
12
12
  'panel.stats': string;
13
+ 'panel.blockPosition': string;
13
14
  'panel.selectHint': string;
15
+ 'panel.searchPlaceholder': string;
14
16
  'panel.missing': string;
15
17
  'panel.missingHint': string;
16
18
  'panel.createHint': string;
@@ -29,6 +31,7 @@ export declare const zh: {
29
31
  'action.langAuto': string;
30
32
  'action.langAutoDetected': string;
31
33
  'action.langSelect': string;
34
+ 'action.search': string;
32
35
  'action.expand': string;
33
36
  'action.exitFullscreen': string;
34
37
  'action.close': string;
@@ -54,7 +57,9 @@ export declare const en: {
54
57
  'panel.group.others': string;
55
58
  'panel.aria': string;
56
59
  'panel.stats': string;
60
+ 'panel.blockPosition': string;
57
61
  'panel.selectHint': string;
62
+ 'panel.searchPlaceholder': string;
58
63
  'panel.missing': string;
59
64
  'panel.missingHint': string;
60
65
  'panel.createHint': string;
@@ -73,6 +78,7 @@ export declare const en: {
73
78
  'action.langAuto': string;
74
79
  'action.langAutoDetected': string;
75
80
  'action.langSelect': string;
81
+ 'action.search': string;
76
82
  'action.expand': string;
77
83
  'action.exitFullscreen': string;
78
84
  'action.close': string;
@@ -5,17 +5,21 @@
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, DiffApprovalOpenAction, DiffApprovalOpenValue, PendingFileDiff } from '../types.ts';
8
+ import type { DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue } 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
- /** Read one session's pending entries, oldest capture first. */
14
- list(sessionId: SessionId): Promise<PendingFileDiff[]>;
13
+ /** Read one session's pending entries (plus its workspace root), oldest capture first. */
14
+ list(sessionId: SessionId): Promise<DiffApprovalListValue>;
15
15
  /** Keep one operation. */
16
16
  keep(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
17
17
  /** Revert one operation. */
18
18
  revert(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
19
+ /** Keep one diff block (accept its change into the tracked baseline). */
20
+ blockKeep(sessionId: SessionId, id: string, block: DiffApprovalBlockRange): Promise<DiffApprovalActionValue>;
21
+ /** Revert one diff block (restore its old lines in the file). */
22
+ blockRevert(sessionId: SessionId, id: string, block: DiffApprovalBlockRange): Promise<DiffApprovalActionValue>;
19
23
  /** Open one file with its default application or reveal it in the folder. */
20
24
  open(sessionId: SessionId, id: string, action: DiffApprovalOpenAction): Promise<DiffApprovalOpenValue>;
21
25
  }
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Reference labels for the selection toolbar: a file name plus a 1-based line
3
- * range, using the short name when unambiguous and the full path otherwise.
4
- * Pure derivation so the display rule is unit-testable without the panel.
2
+ * Reference labels for the selection toolbar: a workspace-relative path (or
3
+ * the absolute path when the file is outside the workspace) plus a 1-based
4
+ * line range. Pure derivation so the display rule is unit-testable without
5
+ * the panel.
5
6
  * @module dsh-diff-approval/client/reference
6
7
  */
7
- import type { PendingFileDiff } from '../types.ts';
8
8
  /**
9
9
  * Last path segment of a file path, any separator style.
10
10
  * @param path - the path to shorten.
@@ -12,13 +12,15 @@ import type { PendingFileDiff } from '../types.ts';
12
12
  */
13
13
  export declare function basenameOf(path: string): string;
14
14
  /**
15
- * The path shown in a copied reference: the short name when no other listed
16
- * file shares it, the full path otherwise.
15
+ * The path embedded in a copied reference: workspace-relative (forward
16
+ * slashes) when the file lives inside the current workspace, the absolute
17
+ * path otherwise. A bare file name is never enough — a reference must resolve
18
+ * to exactly one file.
17
19
  * @param path - the selected file's path.
18
- * @param files - every currently listed pending file.
19
- * @returns the display path for a copied reference.
20
+ * @param workspacePath - the current workspace root, or `undefined`.
21
+ * @returns the reference path.
20
22
  */
21
- export declare function copyDisplayPath(path: string, files: readonly PendingFileDiff[]): string;
23
+ export declare function referencePathOf(path: string, workspacePath: string | undefined): string;
22
24
  /**
23
25
  * Format one line range as a reference suffix: a single line number, or the
24
26
  * inclusive range when the selection spans more than one line.
@@ -30,9 +32,9 @@ export declare function lineRangeLabel(start: number, end: number): string;
30
32
  /**
31
33
  * Build the clipboard text for a selected line range.
32
34
  * @param path - the selected file's path.
33
- * @param files - every currently listed pending file.
35
+ * @param workspacePath - the current workspace root, or `undefined`.
34
36
  * @param start - first selected line number.
35
37
  * @param end - last selected line number.
36
38
  * @returns the `path:range` reference text.
37
39
  */
38
- export declare function referenceOf(path: string, files: readonly PendingFileDiff[], start: number, end: number): string;
40
+ export declare function referenceOf(path: string, workspacePath: string | undefined, start: number, end: number): string;
@@ -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 { DiffApprovalOpenAction, PendingFileDiff } from '../types.ts';
4
+ import type { DiffApprovalBlockRange, DiffApprovalOpenAction, PendingFileDiff } 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. */
@@ -12,6 +12,8 @@ export interface PendingDiffSnapshot {
12
12
  error?: string;
13
13
  /** Entry ids whose keep/revert is in flight; their controls are disabled. */
14
14
  busy: ReadonlySet<string>;
15
+ /** The viewing session's workspace root (when it has one); enables workspace-relative references. */
16
+ workspacePath?: string | undefined;
15
17
  }
16
18
  /** The injected face the panel component receives from the plugin body. */
17
19
  export interface PendingPanelFace {
@@ -25,6 +27,10 @@ export interface PendingPanelFace {
25
27
  onKeep: (sessionId: SessionId, id: string) => Promise<void>;
26
28
  /** Revert one operation (restore its prior content, or remove a created file). */
27
29
  onRevert: (sessionId: SessionId, id: string) => Promise<void>;
30
+ /** Keep one diff block (accept its change into the tracked baseline). */
31
+ onBlockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
32
+ /** Revert one diff block (restore its old lines in the file). */
33
+ onBlockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
28
34
  /** Open one file with its default application or reveal it in the folder. */
29
35
  onOpen: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
30
36
  }
@@ -7,7 +7,7 @@
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 { DiffApprovalOpenAction } from '../types.ts';
10
+ import type { DiffApprovalBlockRange, DiffApprovalOpenAction } 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. */
@@ -18,6 +18,10 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
18
18
  keep: (sessionId: SessionId, id: string) => Promise<void>;
19
19
  /** Revert one operation. */
20
20
  revert: (sessionId: SessionId, id: string) => Promise<void>;
21
+ /** Keep one diff block, then refresh so the entry's diff reflects the accept. */
22
+ blockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
23
+ /** Revert one diff block, then refresh so the entry's diff reflects the undo. */
24
+ blockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
21
25
  /** Open one file with its default application or reveal it in the folder. */
22
26
  open: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
23
27
  /** Drop every local fact (used on connection reset). */
@@ -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, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, PendingEntry, PendingEntryKind, PendingFileDiff, } from './types.ts';
38
+ export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBlockTarget, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, 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';
@@ -55,6 +55,20 @@ export declare class PendingDiffStore {
55
55
  * @returns whether an entry was removed.
56
56
  */
57
57
  remove(sessionId: SessionId, id: string): boolean;
58
+ /**
59
+ * Advance one entry's tracked content after a block-level keep/revert. The
60
+ * entry keeps its id and path; only the given side's text and capture time
61
+ * move. When the caller decides the sides now match, it removes the entry
62
+ * instead of updating it.
63
+ * @param sessionId - the owning session.
64
+ * @param id - the entry id.
65
+ * @param patch - the side to advance (`oldText` for keep, `newText` for revert).
66
+ * @returns whether the entry changed.
67
+ */
68
+ update(sessionId: SessionId, id: string, patch: {
69
+ oldText?: string;
70
+ newText?: string;
71
+ }): boolean;
58
72
  /** Total entry count across all sessions. */
59
73
  get size(): number;
60
74
  }
@@ -51,6 +51,8 @@ export interface PendingFileDiff extends PendingEntry {
51
51
  export interface DiffApprovalListValue {
52
52
  /** Pending entries for the requested session, oldest capture first. */
53
53
  files: PendingFileDiff[];
54
+ /** The viewing session's workspace root (when it has one), for workspace-relative references. */
55
+ workspacePath?: string | undefined;
54
56
  }
55
57
  /** What the open endpoint asks the OS to do with a file. */
56
58
  export type DiffApprovalOpenAction = 'open' | 'reveal';
@@ -59,6 +61,27 @@ export interface DiffApprovalOpenValue {
59
61
  /** What the request did; `missing` means no pending entry existed. */
60
62
  outcome: 'opened' | 'missing';
61
63
  }
64
+ /**
65
+ * One diff block's line ranges on the old and new sides, 1-based inclusive.
66
+ * A side is empty when its start exceeds its end; an empty side's start is
67
+ * the insertion point (the line before which content inserts) on that side.
68
+ */
69
+ export interface DiffApprovalBlockRange {
70
+ /** First old-file line this block spans; the insertion point for pure additions. */
71
+ oldStart: number;
72
+ /** Last old-file line; `oldStart - 1` when the block has no old side. */
73
+ oldEnd: number;
74
+ /** First new-file line this block spans; the insertion point for pure deletions. */
75
+ newStart: number;
76
+ /** Last new-file line; `newStart - 1` when the block has no new side. */
77
+ newEnd: number;
78
+ }
79
+ /** Target of one block-level keep/revert: the entry plus the block range. */
80
+ export interface DiffApprovalBlockTarget {
81
+ sessionId: SessionId;
82
+ id: string;
83
+ block: DiffApprovalBlockRange;
84
+ }
62
85
  /** Outcome of one keep/revert request. */
63
86
  export type DiffApprovalActionOutcome = 'kept' | 'reverted' | 'missing';
64
87
  /** Value returned by the channel's keep and revert endpoints. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",