dsh-diff-approval 0.3.0 → 0.5.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 +20 -5
- package/README.zh.md +20 -5
- package/docs/images/pending-panel.png +0 -0
- package/docs/images/pending-panel.zh.png +0 -0
- package/lib/client.js +659 -220
- package/lib/index.js +112 -23
- package/lib/types/client/highlight.d.ts +12 -0
- package/lib/types/client/locales.d.ts +14 -2
- package/lib/types/index.d.ts +4 -3
- package/lib/types/persist.d.ts +10 -0
- package/package.json +3 -2
package/lib/index.js
CHANGED
|
@@ -236,6 +236,29 @@ var PendingPersistence = class {
|
|
|
236
236
|
return entries.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
237
237
|
}
|
|
238
238
|
/**
|
|
239
|
+
* Load every session's entries for one workspace, oldest capture first.
|
|
240
|
+
* The list shows a workspace's pending changes across sessions — a session
|
|
241
|
+
* that restarted carries a fresh id while its earlier entries sit under the
|
|
242
|
+
* original session ids in the same workspace file — so hydration reads the
|
|
243
|
+
* whole workspace, not one session.
|
|
244
|
+
* @param workspaceId - the owning workspace's stable id.
|
|
245
|
+
* @returns the persisted entries across all sessions; empty when none were saved.
|
|
246
|
+
*/
|
|
247
|
+
async loadWorkspace(workspaceId) {
|
|
248
|
+
const envelope = await this.readWorkspace(this.fileOf(workspaceId));
|
|
249
|
+
if (envelope === void 0) return [];
|
|
250
|
+
const entries = [];
|
|
251
|
+
for (const sessionId of Object.keys(envelope.sessions)) {
|
|
252
|
+
const rows = envelope.sessions[sessionId];
|
|
253
|
+
if (!Array.isArray(rows)) continue;
|
|
254
|
+
for (const row of rows) {
|
|
255
|
+
const entry = pendingEntryOf(row);
|
|
256
|
+
if (entry !== void 0) entries.push(entry);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return entries.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
239
262
|
* Replace one session's entries durably. Saves to one file are serialized;
|
|
240
263
|
* a previous save's failure does not block the next one.
|
|
241
264
|
* @param workspaceId - the owning workspace's stable id.
|
|
@@ -348,9 +371,10 @@ function defaultOpenPath(path, action) {
|
|
|
348
371
|
* ```
|
|
349
372
|
*
|
|
350
373
|
* Pending entries persist per (workspace, session) so an unhandled operation
|
|
351
|
-
* survives a harness restart; the list endpoint
|
|
352
|
-
*
|
|
353
|
-
* restart
|
|
374
|
+
* survives a harness restart; the list endpoint hydrates the whole workspace
|
|
375
|
+
* and merges every registered session's entries, so a fresh session after a
|
|
376
|
+
* restart still reports the earlier sessions' pending changes, live-verified
|
|
377
|
+
* exactly as it is mid-session.
|
|
354
378
|
*
|
|
355
379
|
* @module dsh-diff-approval
|
|
356
380
|
*/
|
|
@@ -460,8 +484,11 @@ function apply(ctx, config) {
|
|
|
460
484
|
const store = new PendingDiffStore();
|
|
461
485
|
const persistence = new PendingPersistence(resolve(expandHomePath(storageDir ?? defaultStorageDir())));
|
|
462
486
|
const launchPath = config?.openPath ?? defaultOpenPath;
|
|
463
|
-
|
|
464
|
-
const
|
|
487
|
+
/** Sessions seen per workspace, so the list can merge a workspace's sessions. */
|
|
488
|
+
const sessionsByWorkspace = /* @__PURE__ */ new Map();
|
|
489
|
+
/** Workspace ids whose persisted state has been hydrated into the store. */
|
|
490
|
+
const loadedWorkspaces = /* @__PURE__ */ new Set();
|
|
491
|
+
const loadingWorkspaces = /* @__PURE__ */ new Map();
|
|
465
492
|
/** Pre-write bases captured at the intent seams, keyed by the tool call id. */
|
|
466
493
|
const editorIntents = /* @__PURE__ */ new Map();
|
|
467
494
|
/**
|
|
@@ -602,31 +629,94 @@ function apply(ctx, config) {
|
|
|
602
629
|
for (const workspace of ctx.workspaceRegistry.list()) if (workspace.sessionIds.includes(sessionId)) return workspace;
|
|
603
630
|
}
|
|
604
631
|
/**
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
* @
|
|
632
|
+
* Record one session in its workspace's account. Every path that touches a
|
|
633
|
+
* session registers it, so the list merges all of a workspace's sessions'
|
|
634
|
+
* entries — a fresh session after restart still sees the workspace's
|
|
635
|
+
* persisted pending changes.
|
|
636
|
+
* @param sessionId - the session to register.
|
|
637
|
+
* @returns the owning workspace, or `undefined` when none accounts it.
|
|
610
638
|
*/
|
|
611
|
-
function
|
|
612
|
-
const
|
|
613
|
-
if (
|
|
614
|
-
const
|
|
639
|
+
function registerSession(sessionId) {
|
|
640
|
+
const workspace = workspaceOf(sessionId);
|
|
641
|
+
if (workspace === void 0) return void 0;
|
|
642
|
+
const key = String(workspace.id);
|
|
643
|
+
const sessions = sessionsByWorkspace.get(key);
|
|
644
|
+
if (sessions === void 0) sessionsByWorkspace.set(key, /* @__PURE__ */ new Set([String(sessionId)]));
|
|
645
|
+
else sessions.add(String(sessionId));
|
|
646
|
+
return workspace;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Merge one workspace's persisted entries into the store, once per
|
|
650
|
+
* workspace. Hydration is workspace-scoped: after a restart the current
|
|
651
|
+
* session has a fresh id while the persisted entries live under their
|
|
652
|
+
* original session ids in the same workspace file, so the whole workspace
|
|
653
|
+
* is loaded and every persisted session is accounted. Concurrent callers
|
|
654
|
+
* share the in-flight load, and folds arriving while the load runs stay
|
|
655
|
+
* safe: `hydrate` never overwrites a live entry.
|
|
656
|
+
* @param workspace - the workspace whose persisted state to merge.
|
|
657
|
+
* @returns resolution after the workspace's persisted state is merged (or skipped).
|
|
658
|
+
*/
|
|
659
|
+
function ensureWorkspaceLoaded(workspace) {
|
|
660
|
+
const key = String(workspace.id);
|
|
661
|
+
if (loadedWorkspaces.has(key)) return Promise.resolve();
|
|
662
|
+
const pending = loadingWorkspaces.get(key);
|
|
615
663
|
if (pending !== void 0) return pending;
|
|
616
664
|
const task = (async () => {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
665
|
+
try {
|
|
666
|
+
const persisted = await persistence.loadWorkspace(key);
|
|
667
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
668
|
+
for (const entry of persisted) {
|
|
669
|
+
const sessionKey = String(entry.sessionId);
|
|
670
|
+
const group = bySession.get(sessionKey);
|
|
671
|
+
if (group === void 0) bySession.set(sessionKey, [entry]);
|
|
672
|
+
else group.push(entry);
|
|
673
|
+
}
|
|
674
|
+
for (const [sessionKey, entries] of bySession) {
|
|
675
|
+
const sessions = sessionsByWorkspace.get(key) ?? /* @__PURE__ */ new Set();
|
|
676
|
+
sessions.add(sessionKey);
|
|
677
|
+
sessionsByWorkspace.set(key, sessions);
|
|
678
|
+
store.hydrate(SessionId(sessionKey), entries);
|
|
679
|
+
}
|
|
620
680
|
} catch (error) {
|
|
621
|
-
ctx.logger.warn(`diff-approval: loading persisted state for
|
|
681
|
+
ctx.logger.warn(`diff-approval: loading persisted state for workspace ${key} failed: ${errorMessage(error)}`);
|
|
622
682
|
}
|
|
623
|
-
|
|
624
|
-
|
|
683
|
+
loadedWorkspaces.add(key);
|
|
684
|
+
loadingWorkspaces.delete(key);
|
|
625
685
|
})();
|
|
626
|
-
|
|
686
|
+
loadingWorkspaces.set(key, task);
|
|
627
687
|
return task;
|
|
628
688
|
}
|
|
629
689
|
/**
|
|
690
|
+
* Merge the session's workspace's persisted state into the store (a session
|
|
691
|
+
* with no workspace is the memory-only edge and has nothing to load).
|
|
692
|
+
* @param sessionId - the session to hydrate for.
|
|
693
|
+
* @returns resolution after the workspace's persisted state is merged.
|
|
694
|
+
*/
|
|
695
|
+
function ensureLoaded(sessionId) {
|
|
696
|
+
const workspace = registerSession(sessionId);
|
|
697
|
+
if (workspace === void 0) return Promise.resolve();
|
|
698
|
+
return ensureWorkspaceLoaded(workspace);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* All entries visible to one session: every registered session of its
|
|
702
|
+
* workspace, merged oldest capture first. This is what makes an unhandled
|
|
703
|
+
* change survive a restart — the new session lists the workspace's whole
|
|
704
|
+
* pending set, its own live folds plus the earlier sessions' persisted
|
|
705
|
+
* entries.
|
|
706
|
+
* @param sessionId - the viewing session.
|
|
707
|
+
* @returns the merged entries; a session with no workspace lists only itself.
|
|
708
|
+
*/
|
|
709
|
+
async function workspaceEntries(sessionId) {
|
|
710
|
+
await ensureLoaded(sessionId);
|
|
711
|
+
const workspace = workspaceOf(sessionId);
|
|
712
|
+
if (workspace === void 0) return store.list(sessionId);
|
|
713
|
+
const sessions = sessionsByWorkspace.get(String(workspace.id));
|
|
714
|
+
if (sessions === void 0) return store.list(sessionId);
|
|
715
|
+
const entries = [];
|
|
716
|
+
for (const sessionKey of sessions) entries.push(...store.list(SessionId(sessionKey)));
|
|
717
|
+
return entries.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
630
720
|
* Mirror one session's entries to disk. A write fault logs a warning and
|
|
631
721
|
* leaves the in-memory view intact: the review flow must not break on a
|
|
632
722
|
* storage fault, and the next successful mutation rewrites the whole file.
|
|
@@ -668,10 +758,9 @@ function apply(ctx, config) {
|
|
|
668
758
|
case "list": {
|
|
669
759
|
const sessionId = sessionOf(payload);
|
|
670
760
|
if (sessionId === void 0) return rpcError("sessionId must be a non-empty string");
|
|
671
|
-
await ensureLoaded(sessionId);
|
|
672
761
|
return {
|
|
673
762
|
ok: true,
|
|
674
|
-
value: { files: await listWithState(
|
|
763
|
+
value: { files: await listWithState(await workspaceEntries(sessionId)) }
|
|
675
764
|
};
|
|
676
765
|
}
|
|
677
766
|
case "keep": {
|
|
@@ -15,6 +15,18 @@ export interface HighlightSpan {
|
|
|
15
15
|
text: string;
|
|
16
16
|
style: CSSProperties;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Primary grammar ids offered in the viewer's language selector, in picker
|
|
20
|
+
* order (alphabetical). Kept explicit instead of deriving from `LANGS`: some
|
|
21
|
+
* `@shikijs/langs` modules are composite grammars whose `.name` arrays expose
|
|
22
|
+
* embedded sub-grammars (e.g. C++ → `[regexp, glsl, cpp-macro, cpp]`,
|
|
23
|
+
* TypeScript → `[typescript, jsx, tsx, graphql]`), which would otherwise leak
|
|
24
|
+
* ids like `regexp`, `glsl`, `haml` into the menu. `LANGS` still registers the
|
|
25
|
+
* full grammars (including their embedded languages) for highlighting.
|
|
26
|
+
*/
|
|
27
|
+
export declare const HIGHLIGHT_LANGS: string[];
|
|
28
|
+
/** Conventional display name for a grammar id, falling back to the id itself. */
|
|
29
|
+
export declare function languageDisplayName(id: string): string;
|
|
18
30
|
/**
|
|
19
31
|
* Tokenize `code` into per-line highlighted runs when `lang` names a
|
|
20
32
|
* registered grammar; `undefined` means the caller renders its plain fallback.
|
|
@@ -24,8 +24,14 @@ export declare const zh: {
|
|
|
24
24
|
'action.busy': string;
|
|
25
25
|
'action.prevDiff': string;
|
|
26
26
|
'action.nextDiff': string;
|
|
27
|
-
'action.
|
|
27
|
+
'action.copyHint': string;
|
|
28
28
|
'action.copied': string;
|
|
29
|
+
'action.langAuto': string;
|
|
30
|
+
'action.langAutoDetected': string;
|
|
31
|
+
'action.langSelect': string;
|
|
32
|
+
'action.expand': string;
|
|
33
|
+
'action.exitFullscreen': string;
|
|
34
|
+
'action.close': string;
|
|
29
35
|
'status.kept': string;
|
|
30
36
|
'status.reverted': string;
|
|
31
37
|
'status.missing': string;
|
|
@@ -62,8 +68,14 @@ export declare const en: {
|
|
|
62
68
|
'action.busy': string;
|
|
63
69
|
'action.prevDiff': string;
|
|
64
70
|
'action.nextDiff': string;
|
|
65
|
-
'action.
|
|
71
|
+
'action.copyHint': string;
|
|
66
72
|
'action.copied': string;
|
|
73
|
+
'action.langAuto': string;
|
|
74
|
+
'action.langAutoDetected': string;
|
|
75
|
+
'action.langSelect': string;
|
|
76
|
+
'action.expand': string;
|
|
77
|
+
'action.exitFullscreen': string;
|
|
78
|
+
'action.close': string;
|
|
67
79
|
'status.kept': string;
|
|
68
80
|
'status.reverted': string;
|
|
69
81
|
'status.missing': string;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
* ```
|
|
27
27
|
*
|
|
28
28
|
* Pending entries persist per (workspace, session) so an unhandled operation
|
|
29
|
-
* survives a harness restart; the list endpoint
|
|
30
|
-
*
|
|
31
|
-
* restart
|
|
29
|
+
* survives a harness restart; the list endpoint hydrates the whole workspace
|
|
30
|
+
* and merges every registered session's entries, so a fresh session after a
|
|
31
|
+
* restart still reports the earlier sessions' pending changes, live-verified
|
|
32
|
+
* exactly as it is mid-session.
|
|
32
33
|
*
|
|
33
34
|
* @module dsh-diff-approval
|
|
34
35
|
*/
|
package/lib/types/persist.d.ts
CHANGED
|
@@ -37,6 +37,16 @@ export declare class PendingPersistence {
|
|
|
37
37
|
* @returns the persisted entries; empty when none were saved.
|
|
38
38
|
*/
|
|
39
39
|
load(workspaceId: string, sessionId: string): Promise<PendingEntry[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Load every session's entries for one workspace, oldest capture first.
|
|
42
|
+
* The list shows a workspace's pending changes across sessions — a session
|
|
43
|
+
* that restarted carries a fresh id while its earlier entries sit under the
|
|
44
|
+
* original session ids in the same workspace file — so hydration reads the
|
|
45
|
+
* whole workspace, not one session.
|
|
46
|
+
* @param workspaceId - the owning workspace's stable id.
|
|
47
|
+
* @returns the persisted entries across all sessions; empty when none were saved.
|
|
48
|
+
*/
|
|
49
|
+
loadWorkspace(workspaceId: string): Promise<PendingEntry[]>;
|
|
40
50
|
/**
|
|
41
51
|
* Replace one session's entries durably. Saves to one file are serialized;
|
|
42
52
|
* a previous save's failure does not block the next one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-diff-approval",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"lib/index.js",
|
|
32
32
|
"lib/client.js",
|
|
33
33
|
"lib/types/**/*.d.ts",
|
|
34
|
-
"cordis.patch.yml"
|
|
34
|
+
"cordis.patch.yml",
|
|
35
|
+
"docs/images/**"
|
|
35
36
|
],
|
|
36
37
|
"dsh": {
|
|
37
38
|
"bundle": {
|