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/README.md +4 -3
- package/README.zh.md +4 -3
- package/docs/images/pending-panel.png +0 -0
- package/docs/images/pending-panel.zh.png +0 -0
- package/lib/client.js +739 -189
- package/lib/index.js +104 -25
- package/lib/types/client/PendingPanel.d.ts +2 -1
- package/lib/types/client/SettingsTab.d.ts +5 -4
- package/lib/types/client/locales.d.ts +14 -0
- package/lib/types/client/settings.d.ts +19 -0
- package/lib/types/client/slots.d.ts +6 -0
- package/lib/types/client/store.d.ts +2 -0
- package/lib/types/types.d.ts +4 -0
- package/package.json +1 -1
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
|
|
869
|
-
*
|
|
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
|
-
|
|
880
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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 =
|
|
995
|
+
const state = {
|
|
919
996
|
missing: false,
|
|
920
|
-
diverged:
|
|
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
|
|
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
|
|
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,
|
|
7
|
-
* whether importing workspace VCS changes includes untracked files
|
|
8
|
-
* mirrors the harness's Agent-preset row (title +
|
|
9
|
-
* pill picker on the right)
|
|
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.
|
package/lib/types/types.d.ts
CHANGED
|
@@ -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