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/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 re-reads the live file, so a
352
- * change or deletion made after the tracked operation is reported after
353
- * restart exactly as it is mid-session.
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
- const loaded = /* @__PURE__ */ new Set();
464
- const loading = /* @__PURE__ */ new Map();
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
- * Merge one session's persisted entries into the store, once per session.
606
- * Concurrent callers share the in-flight load, and folds arriving while the
607
- * load runs stay safe: `hydrate` never overwrites a live entry.
608
- * @param sessionId - the session to hydrate.
609
- * @returns resolution after the session's persisted state is merged (or skipped).
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 ensureLoaded(sessionId) {
612
- const key = String(sessionId);
613
- if (loaded.has(key)) return Promise.resolve();
614
- const pending = loading.get(key);
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
- const workspace = workspaceOf(sessionId);
618
- if (workspace !== void 0) try {
619
- store.hydrate(sessionId, await persistence.load(String(workspace.id), key));
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 session ${key} failed: ${errorMessage(error)}`);
681
+ ctx.logger.warn(`diff-approval: loading persisted state for workspace ${key} failed: ${errorMessage(error)}`);
622
682
  }
623
- loaded.add(key);
624
- loading.delete(key);
683
+ loadedWorkspaces.add(key);
684
+ loadingWorkspaces.delete(key);
625
685
  })();
626
- loading.set(key, task);
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(store.list(sessionId)) }
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.copyRange': string;
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.copyRange': string;
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;
@@ -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 re-reads the live file, so a
30
- * change or deletion made after the tracked operation is reported after
31
- * restart exactly as it is mid-session.
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
  */
@@ -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.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": {