dsh-diff-approval 0.8.0 → 0.10.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
@@ -864,9 +864,16 @@ function apply(ctx, config) {
864
864
  await ensureLoaded(sessionId);
865
865
  if (store.fold(entry)) await persistSession(sessionId);
866
866
  }
867
+ /** The stable `FsError.code`, when the thrown value carries one. */
868
+ function fsErrorCodeOf(error) {
869
+ if (typeof error !== "object" || error === null) return void 0;
870
+ const code = error.code;
871
+ return typeof code === "string" ? code : void 0;
872
+ }
867
873
  /**
868
- * Read one path's live state: present content, an unresolvable (missing)
869
- * path, or a resolved-but-unreadable file.
874
+ * Read one path's live state. The only existence test is `stat`: it returns
875
+ * `undefined` for an absent target (gone), so a deleted file never falls into
876
+ * the unreadable bucket. A file that exists but cannot be read is `unavailable`.
870
877
  * @param path - backend display path to probe through `ctx.fs`.
871
878
  * @returns the live state.
872
879
  */
@@ -874,22 +881,23 @@ function apply(ctx, config) {
874
881
  let target;
875
882
  try {
876
883
  target = await ctx.fs.resolve(path, {});
877
- } catch {
878
- return {
879
- present: false,
880
- kind: "missing"
881
- };
884
+ } catch (error) {
885
+ return fsErrorCodeOf(error) === "FS_NOT_FOUND" ? { kind: "deleted" } : { kind: "unavailable" };
886
+ }
887
+ let info;
888
+ try {
889
+ info = await ctx.fs.stat(target, void 0);
890
+ } catch (error) {
891
+ return fsErrorCodeOf(error) === "FS_NOT_FOUND" ? { kind: "deleted" } : { kind: "unavailable" };
882
892
  }
893
+ if (info === void 0) return { kind: "deleted" };
883
894
  try {
884
895
  return {
885
- present: true,
896
+ kind: "present",
886
897
  content: await ctx.fs.readText(target, void 0)
887
898
  };
888
- } catch {
889
- return {
890
- present: false,
891
- kind: "unreadable"
892
- };
899
+ } catch (error) {
900
+ return fsErrorCodeOf(error) === "FS_NOT_FOUND" ? { kind: "deleted" } : { kind: "unavailable" };
893
901
  }
894
902
  }
895
903
  /**
@@ -898,7 +906,32 @@ function apply(ctx, config) {
898
906
  * @param entries - the store's entries for one session.
899
907
  * @returns entries with `missing` and `diverged` set from the live file.
900
908
  */
901
- async function listWithState(entries) {
909
+ /** Drop every undo/redo pair that belongs to a removed entry, so the LIFO
910
+ * queue stays traversable without ever trying to restore an unreadable file. */
911
+ function purgeForEntry(sessionId, entryId, path) {
912
+ const key = String(sessionId);
913
+ for (const stacks of [undoStacks, redoStacks]) {
914
+ const stack = stacks.get(key);
915
+ if (stack === void 0) continue;
916
+ stacks.set(key, stack.filter((pair) => {
917
+ const touches = (state) => state.id === entryId || state.path === path;
918
+ const batch = pair.before.batch ?? pair.after.batch;
919
+ if (batch !== void 0) return !batch.some((item) => item.id === entryId || item.path === path);
920
+ return !touches(pair.before) && !touches(pair.after);
921
+ }));
922
+ }
923
+ }
924
+ /**
925
+ * Settle each listed entry against its live file. Existence is decided by the
926
+ * live state alone: a deleted file leaves the list as an undoable checkpoint
927
+ * (Ctrl+Z recreates it and restores the entry), an unavailable one leaves the
928
+ * list and has its undo/redo records dropped, and externally changed content
929
+ * is adopted as the new baseline with its own checkpoint.
930
+ * @param sessionId - the session being listed.
931
+ * @param entries - the store's entries for one session.
932
+ * @returns the listed entries plus whether an external change cleared redo.
933
+ */
934
+ async function listWithState(sessionId, entries) {
902
935
  const byPath = /* @__PURE__ */ new Map();
903
936
  for (const entry of entries) {
904
937
  const group = byPath.get(entry.path);
@@ -906,21 +939,62 @@ function apply(ctx, config) {
906
939
  else group.push(entry);
907
940
  }
908
941
  const listed = [];
942
+ let redoCleared = false;
909
943
  for (const group of byPath.values()) {
910
944
  const newest = group[group.length - 1];
911
945
  if (newest === void 0) continue;
912
946
  const live = await liveStateOf(newest.path);
947
+ if (live.kind === "deleted") {
948
+ store.remove(sessionId, newest.id);
949
+ pushUndo(sessionId, {
950
+ id: newest.id,
951
+ path: newest.path,
952
+ entry: newest,
953
+ fileText: newest.newText
954
+ }, {
955
+ id: newest.id,
956
+ path: newest.path,
957
+ entry: void 0,
958
+ fileText: void 0
959
+ });
960
+ await persistSession(sessionId);
961
+ continue;
962
+ }
963
+ if (live.kind === "unavailable") {
964
+ store.remove(sessionId, newest.id);
965
+ purgeForEntry(sessionId, newest.id, newest.path);
966
+ await persistSession(sessionId);
967
+ continue;
968
+ }
969
+ const content = live.content;
913
970
  let adopted = newest.newText;
914
- if (live.present && typeof live.content === "string" && live.content !== newest.newText) {
915
- adopted = live.content;
916
- if (store.update(newest.sessionId, newest.id, { newText: live.content })) await persistSession(newest.sessionId);
971
+ const hasContent = typeof content === "string";
972
+ if (hasContent && content !== newest.newText) {
973
+ adopted = content;
974
+ const beforeText = newest.newText;
975
+ const redoWasPresent = redoStacks.has(String(sessionId));
976
+ store.update(sessionId, newest.id, { newText: content });
977
+ pushUndo(sessionId, {
978
+ id: newest.id,
979
+ path: newest.path,
980
+ entry: newest,
981
+ fileText: beforeText
982
+ }, {
983
+ id: newest.id,
984
+ path: newest.path,
985
+ entry: {
986
+ ...newest,
987
+ newText: content,
988
+ updatedAt: Date.now()
989
+ },
990
+ fileText: content
991
+ });
992
+ await persistSession(sessionId);
993
+ if (redoWasPresent) redoCleared = true;
917
994
  }
918
- const state = live.present ? {
995
+ const state = {
919
996
  missing: false,
920
- diverged: live.content !== adopted
921
- } : {
922
- missing: live.kind === "missing",
923
- diverged: live.kind === "unreadable"
997
+ diverged: hasContent ? content !== adopted : true
924
998
  };
925
999
  for (const entry of group) listed.push(entry.id === newest.id ? {
926
1000
  ...entry,
@@ -931,7 +1005,10 @@ function apply(ctx, config) {
931
1005
  ...state
932
1006
  });
933
1007
  }
934
- return listed;
1008
+ return {
1009
+ files: listed,
1010
+ redoCleared
1011
+ };
935
1012
  }
936
1013
  /**
937
1014
  * The workspace whose session account holds `sessionId`. Web sessions are
@@ -1146,11 +1223,13 @@ function apply(ctx, config) {
1146
1223
  case "list": {
1147
1224
  const sessionId = sessionOf(payload);
1148
1225
  if (sessionId === void 0) return rpcError("sessionId must be a non-empty string");
1226
+ const { files, redoCleared } = await listWithState(sessionId, await workspaceEntries(sessionId));
1149
1227
  return {
1150
1228
  ok: true,
1151
1229
  value: {
1152
- files: await listWithState(await workspaceEntries(sessionId)),
1153
- workspacePath: workspaceOf(sessionId)?.path
1230
+ files,
1231
+ workspacePath: workspaceOf(sessionId)?.path,
1232
+ redoCleared: redoCleared || void 0
1154
1233
  }
1155
1234
  };
1156
1235
  }
@@ -1,6 +1,7 @@
1
1
  /** Sidebar-foot pending-edit review action and the split review panel it opens. */
2
2
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
3
  import type { PendingPanelFace } from './slots.ts';
4
+ export declare function wrapInto(text: string, widthPx: number, measure: (t: string) => number, tabPx: number): string[];
4
5
  /** Full panel props composed by the sidebar footer-action slot. */
5
6
  export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<PendingPanelFace> & PropsLocale<'diff-approval'>;
6
7
  /**
@@ -14,4 +15,4 @@ export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFa
14
15
  */
15
16
  export declare function openSettingsSection(sectionLabel: string): void;
16
17
  /** Render the pending-edit review panel and its unified footer action. */
17
- export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPasteReference, onUndo, onRedo, onImportVcs, t, }: PendingPanelProps): import("react").JSX.Element;
18
+ export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPasteReference, onUndo, onRedo, onImportVcs, onAckRedoCleared, t, }: PendingPanelProps): import("react").JSX.Element;
@@ -3,9 +3,10 @@ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots
3
3
  /** Full component props assembled by the Settings slot renderer. */
4
4
  export type DiffApprovalSettingsTabProps = PropsRuntime<'settings.section'> & PropsLocale<'diff-approval'>;
5
5
  /**
6
- * The plugin's preferences: auto-paste a copied reference into the input, and
7
- * whether importing workspace VCS changes includes untracked files. Each row
8
- * mirrors the harness's Agent-preset row (title + description on the left, a
9
- * pill picker on the right) with the two boolean states 打开 / 关闭.
6
+ * The plugin's preferences: auto-paste a copied reference into the input,
7
+ * whether importing workspace VCS changes includes untracked files, and the
8
+ * diff's tab width. Each row mirrors the harness's Agent-preset row (title +
9
+ * description on the left, a pill picker on the right); the tab-width row
10
+ * offers 2 / 4 / 8 spaces.
10
11
  */
11
12
  export declare function DiffApprovalSettingsTab({ t }: DiffApprovalSettingsTabProps): import("react").JSX.Element;
@@ -15,11 +15,15 @@ export declare const zh: {
15
15
  'panel.searchPlaceholder': string;
16
16
  'panel.missing': string;
17
17
  'panel.missingHint': string;
18
+ 'panel.externalChanged': string;
19
+ 'panel.dismiss': string;
18
20
  'panel.createHint': string;
19
21
  'panel.pasteOnCopy': string;
20
22
  'panel.pasteOnCopyDesc': string;
21
23
  'panel.importUntracked': string;
22
24
  'panel.importUntrackedDesc': string;
25
+ 'panel.tabWidth': string;
26
+ 'panel.tabWidthDesc': string;
23
27
  'settings.tabLabel': string;
24
28
  'row.create': string;
25
29
  'row.failed': string;
@@ -34,11 +38,14 @@ export declare const zh: {
34
38
  'action.busy': string;
35
39
  'action.prevDiff': string;
36
40
  'action.nextDiff': string;
41
+ 'action.showFileList': string;
42
+ 'action.hideFileList': string;
37
43
  'action.copyHint': string;
38
44
  'action.copied': string;
39
45
  'action.langAuto': string;
40
46
  'action.langAutoDetected': string;
41
47
  'action.langSelect': string;
48
+ 'action.wrap': string;
42
49
  'action.toggleOn': string;
43
50
  'action.toggleOff': string;
44
51
  'action.importVcs': string;
@@ -79,11 +86,15 @@ export declare const en: {
79
86
  'panel.searchPlaceholder': string;
80
87
  'panel.missing': string;
81
88
  'panel.missingHint': string;
89
+ 'panel.externalChanged': string;
90
+ 'panel.dismiss': string;
82
91
  'panel.createHint': string;
83
92
  'panel.pasteOnCopy': string;
84
93
  'panel.pasteOnCopyDesc': string;
85
94
  'panel.importUntracked': string;
86
95
  'panel.importUntrackedDesc': string;
96
+ 'panel.tabWidth': string;
97
+ 'panel.tabWidthDesc': string;
87
98
  'settings.tabLabel': string;
88
99
  'row.create': string;
89
100
  'row.failed': string;
@@ -98,11 +109,14 @@ export declare const en: {
98
109
  'action.busy': string;
99
110
  'action.prevDiff': string;
100
111
  'action.nextDiff': string;
112
+ 'action.showFileList': string;
113
+ 'action.hideFileList': string;
101
114
  'action.copyHint': string;
102
115
  'action.copied': string;
103
116
  'action.langAuto': string;
104
117
  'action.langAutoDetected': string;
105
118
  'action.langSelect': string;
119
+ 'action.wrap': string;
106
120
  'action.toggleOn': string;
107
121
  'action.toggleOff': string;
108
122
  'action.importVcs': string;
@@ -17,3 +17,22 @@ export declare function setPasteOnCopyEnabled(value: boolean): void;
17
17
  export declare function includeUntrackedEnabled(): boolean;
18
18
  /** Persist the import-untracked preference. */
19
19
  export declare function setIncludeUntrackedEnabled(value: boolean): void;
20
+ /**
21
+ * Whether lines wrap (auto-wrap) in the diff for one highlight language.
22
+ * Defaults to off; only an explicit `'1'` enables it. Stored per language, so
23
+ * a language's preference never leaks into another's.
24
+ * @param lang - the highlight language (or `''` for the auto/default bucket).
25
+ * @returns whether lines wrap.
26
+ */
27
+ export declare function wrapEnabled(lang: string): boolean;
28
+ /** Persist the per-language auto-wrap preference. */
29
+ export declare function setWrapEnabled(lang: string, value: boolean): void;
30
+ /**
31
+ * The diff's tab width in spaces. Defaults to 4; the settings UI offers 2/4/8,
32
+ * but any positive integer is accepted. This drives both the rendered
33
+ * `tab-size` and the wrapped-line tab measurement, so they always agree.
34
+ * @returns the number of spaces one tab advances.
35
+ */
36
+ export declare function tabWidth(): number;
37
+ /** Persist the diff's tab width (in spaces). */
38
+ export declare function setTabWidth(value: number): void;
@@ -16,6 +16,10 @@ export interface PendingDiffSnapshot {
16
16
  failed?: ReadonlyMap<string, string> | undefined;
17
17
  /** The viewing session's workspace root (when it has one); enables workspace-relative references. */
18
18
  workspacePath?: string | undefined;
19
+ /** Latched when an external change created a fresh undo checkpoint that
20
+ * superseded the redo history; the panel surfaces it once (deferred if the
21
+ * panel is closed) via a bottom-right notice. */
22
+ redoCleared?: boolean;
19
23
  }
20
24
  /** The injected face the panel component receives from the plugin body. */
21
25
  export interface PendingPanelFace {
@@ -43,4 +47,6 @@ export interface PendingPanelFace {
43
47
  onRedo: (sessionId: SessionId) => Promise<string | undefined>;
44
48
  /** Import the workspace's local VCS changes as pending entries (detection included). */
45
49
  onImportVcs: (sessionId: SessionId, includeUntracked: boolean) => Promise<VcsImportValue>;
50
+ /** Acknowledge the redo-cleared notice so it is only surfaced once. */
51
+ onAckRedoCleared: () => void;
46
52
  }
@@ -32,6 +32,8 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
32
32
  open: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
33
33
  /** Drop every local fact (used on connection reset). */
34
34
  reset: () => void;
35
+ /** Acknowledge a redo-cleared notice so the panel surfaces it only once. */
36
+ clearRedoCleared: () => void;
35
37
  }
36
38
  /**
37
39
  * Create the store over the review-channel port.
@@ -53,6 +53,10 @@ export interface DiffApprovalListValue {
53
53
  files: PendingFileDiff[];
54
54
  /** The viewing session's workspace root (when it has one), for workspace-relative references. */
55
55
  workspacePath?: string | undefined;
56
+ /** Set when a detected external change created a fresh undo checkpoint while
57
+ * redo history was pending, so the panel can surface that the redo stack was
58
+ * superseded. */
59
+ redoCleared?: boolean | undefined;
56
60
  }
57
61
  /** What the open endpoint asks the OS to do with a file. */
58
62
  export type DiffApprovalOpenAction = 'open' | 'reveal';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.8.0",
3
+ "version": "0.10.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",