dsh-diff-approval 0.12.0 → 0.13.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
@@ -1343,18 +1343,12 @@ function apply(ctx, config) {
1343
1343
  };
1344
1344
  const accepted = contentRangeOf(entry.newText, blockTarget.block.newStart, blockTarget.block.newEnd);
1345
1345
  const updatedOld = replaceContentLines(entry.oldText, blockTarget.block.oldStart, blockTarget.block.oldEnd, accepted);
1346
- let afterEntry;
1347
- if (updatedOld === entry.newText) {
1348
- store.remove(blockTarget.sessionId, blockTarget.id);
1349
- afterEntry = void 0;
1350
- } else {
1351
- store.update(blockTarget.sessionId, blockTarget.id, { oldText: updatedOld });
1352
- afterEntry = {
1353
- ...entry,
1354
- oldText: updatedOld,
1355
- updatedAt: Date.now()
1356
- };
1357
- }
1346
+ store.update(blockTarget.sessionId, blockTarget.id, { oldText: updatedOld });
1347
+ const afterEntry = {
1348
+ ...entry,
1349
+ oldText: updatedOld,
1350
+ updatedAt: Date.now()
1351
+ };
1358
1352
  pushUndo(blockTarget.sessionId, {
1359
1353
  id: entry.id,
1360
1354
  path: entry.path,
@@ -1369,7 +1363,10 @@ function apply(ctx, config) {
1369
1363
  await persistSession(blockTarget.sessionId);
1370
1364
  return {
1371
1365
  ok: true,
1372
- value: { outcome: "kept" }
1366
+ value: updatedOld === entry.newText ? {
1367
+ outcome: "kept",
1368
+ resolved: true
1369
+ } : { outcome: "kept" }
1373
1370
  };
1374
1371
  }
1375
1372
  case "block-revert": {
@@ -1384,25 +1381,19 @@ function apply(ctx, config) {
1384
1381
  const restored = contentRangeOf(entry.oldText, blockTarget.block.oldStart, blockTarget.block.oldEnd);
1385
1382
  const updatedNew = replaceContentLines(entry.newText, blockTarget.block.newStart, blockTarget.block.newEnd, restored);
1386
1383
  const content = reencodeEol(updatedNew, detectEol(entry.newText));
1387
- let afterEntry;
1388
- if (normalizeEol(updatedNew) === normalizeEol(entry.oldText)) {
1389
- store.remove(blockTarget.sessionId, blockTarget.id);
1390
- afterEntry = void 0;
1391
- } else {
1392
- store.update(blockTarget.sessionId, blockTarget.id, { newText: content });
1393
- afterEntry = {
1394
- ...entry,
1395
- newText: content,
1396
- updatedAt: Date.now()
1397
- };
1398
- }
1384
+ store.update(blockTarget.sessionId, blockTarget.id, { newText: content });
1385
+ const afterEntry = {
1386
+ ...entry,
1387
+ newText: content,
1388
+ updatedAt: Date.now()
1389
+ };
1399
1390
  let undo;
1400
1391
  try {
1401
- const resolved = await ctx.fs.resolve(entry.path, { signal });
1402
- if (entry.kind === "create" && content === "") await rm(ctx.fs.processPath(resolved), { force: true });
1392
+ const target = await ctx.fs.resolve(entry.path, { signal });
1393
+ if (entry.kind === "create" && content === "") await rm(ctx.fs.processPath(target), { force: true });
1403
1394
  else {
1404
- const preWrite = await ctx.fs.readText(resolved, void 0) ?? entry.newText;
1405
- await writeRevert(resolved, content, blockTarget.sessionId, signal);
1395
+ const preWrite = await ctx.fs.readText(target, void 0) ?? entry.newText;
1396
+ await writeRevert(target, content, blockTarget.sessionId, signal);
1406
1397
  undo = {
1407
1398
  before: {
1408
1399
  id: entry.id,
@@ -1425,7 +1416,10 @@ function apply(ctx, config) {
1425
1416
  await persistSession(blockTarget.sessionId);
1426
1417
  return {
1427
1418
  ok: true,
1428
- value: { outcome: "reverted" }
1419
+ value: normalizeEol(updatedNew) === normalizeEol(entry.oldText) ? {
1420
+ outcome: "reverted",
1421
+ resolved: true
1422
+ } : { outcome: "reverted" }
1429
1423
  };
1430
1424
  }
1431
1425
  case "undo": {
@@ -6,6 +6,10 @@ import type { PendingPanelFace } from './slots.ts';
6
6
  import type { DiffApprovalKey } from './locales.ts';
7
7
  import { computeWholeFileDiff } from './whole-file-diff.ts';
8
8
  import { highlightLines } from './highlight.ts';
9
+ /** The dsh shell's sidebar auto-collapse breakpoint (ui-layout columns.ts):
10
+ * below it the sidebar auto-collapses, and the file list floats on the same
11
+ * breakpoint so the two stay consistent. */
12
+ export declare const SIDEBAR_AUTO_COLLAPSE_PX = 1024;
9
13
  export declare function wrapInto(text: string, widthPx: number, measure: (t: string) => number, tabPx: number): string[];
10
14
  /** Full panel props composed by the sidebar footer-action slot. */
11
15
  export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<PendingPanelFace> & PropsLocale<'diff-approval'>;
@@ -56,5 +60,5 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
56
60
  onBlockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
57
61
  } & import("react").RefAttributes<SplitDiffHandle>>;
58
62
  /** Render the pending-edit review panel and its unified footer action. */
59
- export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPasteReference, onUndo, onRedo, onImportVcs, onAckRedoCleared, t, }: PendingPanelProps): import("react").JSX.Element;
63
+ export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPasteReference, onUndo, onRedo, onImportVcs, onAckRedoCleared, onAckJustResolved, collapseSidebar, t, }: PendingPanelProps): import("react").JSX.Element;
60
64
  export {};
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Scope-addressed access to the session's composer draft and pending queue.
3
+ * Extracted from the plugin entry so the wiring (not just the remap math) is
4
+ * unit-testable against a faithful fake of the DSH conversation service.
5
+ * @module dsh-diff-approval/client/conversation-access
6
+ */
7
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
8
+ import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
9
+ /** One queued message's minimal shape read from the session input state. */
10
+ export interface QueueMessageView {
11
+ id: string;
12
+ placement: 'queued' | 'steering' | 'context';
13
+ content: readonly {
14
+ type: string;
15
+ text?: string;
16
+ }[];
17
+ }
18
+ /** The conversation service slice the reference remap addresses through a session scope. */
19
+ export interface ConversationFace {
20
+ input: {
21
+ for(actx: ClientContext): {
22
+ setDraft(text: string): void;
23
+ state: {
24
+ getSnapshot(): {
25
+ queue: readonly QueueMessageView[];
26
+ };
27
+ };
28
+ };
29
+ };
30
+ updateQueue(itemId: string, action: {
31
+ kind: 'edit';
32
+ content: readonly {
33
+ type: string;
34
+ text?: string;
35
+ }[];
36
+ }): Promise<void>;
37
+ }
38
+ /**
39
+ * Resolve the current session's scoped conversation face. The scoped context
40
+ * (`sessions.scope(id)`) carries no `conversation` inject of its own, so reading
41
+ * it as a property (`actx.conversation`) trips Cordis's inject check and throws
42
+ * `cannot get property "conversation" without inject`. `actx.get('conversation')`
43
+ * bypasses that check yet still returns the traceable bound to `actx`, so its
44
+ * verbs (e.g. `updateQueue`) resolve the right session scope.
45
+ */
46
+ export declare function resolveConversation(ctx: ClientContext, sessionId: SessionId | undefined): {
47
+ actx: ClientContext;
48
+ conversation: ConversationFace;
49
+ } | undefined;
50
+ /** Draft/queue verbs bound to the current session, resolved lazily per call. */
51
+ export interface ConversationAccess {
52
+ /** Write the current session's composer draft. */
53
+ writeDraft: (text: string) => void;
54
+ /** Read the current session's still-queued messages (queued placement only). */
55
+ readQueue: () => readonly QueueMessageView[];
56
+ /** Apply an edit mutation to one queued message's full content. */
57
+ writeQueue: (itemId: string, content: readonly {
58
+ type: string;
59
+ text?: string;
60
+ }[]) => void;
61
+ }
62
+ /**
63
+ * Build the per-call-resolved draft/queue accessor. `sessionId` is a function
64
+ * so the accessor always addresses the *current* session, even though it is
65
+ * handed to the remap sync once.
66
+ */
67
+ export declare function conversationAccess(ctx: ClientContext, sessionId: () => SessionId | undefined): ConversationAccess;
@@ -4,7 +4,8 @@ export type { PendingPanelProps } from './PendingPanel.tsx';
4
4
  export type { PendingDiffSnapshot, PendingPanelFace } from './slots.ts';
5
5
  export type { DiffApprovalKey } from './locales.ts';
6
6
  export { DIFF_APPROVAL_CHANNEL } from './port.ts';
7
- /** Required services: locale, slots, the wire channel, and the current session. */
7
+ /** Required services: locale, slots, the wire channel, the current session, and
8
+ * the layout controller (this plugin collapses the sidebar before its modal opens). */
8
9
  export declare const inject: string[];
9
10
  /**
10
11
  * Mount the pending-edit review panel.
@@ -26,9 +26,13 @@ export declare const zh: {
26
26
  'panel.tabWidthDesc': string;
27
27
  'panel.splitMode': string;
28
28
  'panel.splitModeDesc': string;
29
+ 'panel.quickSummon': string;
30
+ 'panel.quickSummonDesc': string;
31
+ 'panel.recordShortcut': string;
29
32
  'settings.tabLabel': string;
30
33
  'row.create': string;
31
34
  'row.failed': string;
35
+ 'row.dismiss': string;
32
36
  'row.added': string;
33
37
  'row.removed': string;
34
38
  'action.keep': string;
@@ -64,6 +68,8 @@ export declare const zh: {
64
68
  'status.kept': string;
65
69
  'status.reverted': string;
66
70
  'status.missing': string;
71
+ 'panel.resolvedAsk': string;
72
+ 'panel.keepInList': string;
67
73
  };
68
74
  /** Translation keys owned by the pending-edit review namespace. */
69
75
  export type DiffApprovalKey = keyof typeof zh;
@@ -99,9 +105,13 @@ export declare const en: {
99
105
  'panel.tabWidthDesc': string;
100
106
  'panel.splitMode': string;
101
107
  'panel.splitModeDesc': string;
108
+ 'panel.quickSummon': string;
109
+ 'panel.quickSummonDesc': string;
110
+ 'panel.recordShortcut': string;
102
111
  'settings.tabLabel': string;
103
112
  'row.create': string;
104
113
  'row.failed': string;
114
+ 'row.dismiss': string;
105
115
  'row.added': string;
106
116
  'row.removed': string;
107
117
  'action.keep': string;
@@ -137,4 +147,6 @@ export declare const en: {
137
147
  'status.kept': string;
138
148
  'status.reverted': string;
139
149
  'status.missing': string;
150
+ 'panel.resolvedAsk': string;
151
+ 'panel.keepInList': string;
140
152
  };
@@ -5,6 +5,8 @@
5
5
  * the panel.
6
6
  * @module dsh-diff-approval/client/reference
7
7
  */
8
+ /** The marker that replaces a reference's line number when its lines are gone. */
9
+ export declare const LINE_MISSING_LABEL = "LINE_MISSING";
8
10
  /**
9
11
  * Last path segment of a file path, any separator style.
10
12
  * @param path - the path to shorten.
@@ -30,11 +32,39 @@ export declare function referencePathOf(path: string, workspacePath: string | un
30
32
  */
31
33
  export declare function lineRangeLabel(start: number, end: number): string;
32
34
  /**
33
- * Build the clipboard text for a selected line range.
35
+ * Build the clipboard text for a selected line range, wrapped in parentheses so
36
+ * the reference reads as one unambiguous token (and can be matched precisely).
34
37
  * @param path - the selected file's path.
35
38
  * @param workspacePath - the current workspace root, or `undefined`.
36
39
  * @param start - first selected line number.
37
40
  * @param end - last selected line number.
38
- * @returns the `path:range` reference text.
41
+ * @returns the `(path:range)` reference text.
39
42
  */
40
43
  export declare function referenceOf(path: string, workspacePath: string | undefined, start: number, end: number): string;
44
+ /**
45
+ * Map one referenced line range from `oldContent` coordinates to `newContent`
46
+ * coordinates. Lines that survive (unchanged context) map to their new line
47
+ * numbers, and the surviving lines' min/max span is returned. When every line
48
+ * in the range was removed, returns `undefined` (the reference is expired).
49
+ * @param oldContent - the file content the reference was made against.
50
+ * @param newContent - the file content now.
51
+ * @param start - first referenced line (1-based, inclusive).
52
+ * @param end - last referenced line (1-based, inclusive).
53
+ * @returns the surviving range, or `undefined` when nothing survives.
54
+ */
55
+ export declare function remapReferenceRange(oldContent: string, newContent: string, start: number, end: number): {
56
+ start: number;
57
+ end: number;
58
+ } | undefined;
59
+ /**
60
+ * Rewrite every `(referencePath:line)` / `(referencePath:start-end)` occurrence
61
+ * in `text`, remapping each range from `oldContent` to `newContent`. A range
62
+ * whose lines all survived becomes the new range; one whose lines were all
63
+ * removed becomes `(referencePath:LINE_MISSING)`.
64
+ * @param text - the free text (composer draft, queued message) to rewrite.
65
+ * @param referencePath - the file's reference path (workspace-relative or absolute).
66
+ * @param oldContent - the file content the references were made against.
67
+ * @param newContent - the file content now.
68
+ * @returns the rewritten text.
69
+ */
70
+ export declare function remapReferences(text: string, referencePath: string, oldContent: string, newContent: string): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Reference remapping sync: track each pending file's last-seen content and,
3
+ * when it changes (an agent edit, a block revert, or an external adoption),
4
+ * rewrite stale references in the current composer draft and the pending queue.
5
+ * Pure orchestration over the pending store; the actual line remapping lives in
6
+ * `reference.ts`.
7
+ * @module dsh-diff-approval/client/remap-sync
8
+ */
9
+ import type { PendingDiffStore } from './store.ts';
10
+ /** One queued message's minimal content block; text blocks carry `text`, and every other kind passes through verbatim. */
11
+ interface QueueBlock {
12
+ type: string;
13
+ text?: string;
14
+ }
15
+ /** A queued message's minimal shape for reference remapping. */
16
+ interface QueueMessage {
17
+ id: string;
18
+ content: readonly QueueBlock[];
19
+ }
20
+ /** One remapping sync's inputs; `readDraft`/`writeDraft` address the current composer. */
21
+ export interface ReferenceRemapOpts {
22
+ store: PendingDiffStore;
23
+ /** Read the current composer draft text (undefined when there is none). */
24
+ readDraft: () => string | undefined;
25
+ /** Write the current composer draft text. */
26
+ writeDraft: (text: string) => void;
27
+ /** Read the current session's still-queued messages (full content blocks). */
28
+ readQueue: () => readonly QueueMessage[];
29
+ /** Apply an edit mutation to one queued message's full content. */
30
+ writeQueue: (itemId: string, content: readonly QueueBlock[]) => void;
31
+ }
32
+ /**
33
+ * Subscribe to the pending store and remap references as each file's content
34
+ * changes. The first observation of a file only seeds the baseline; a later
35
+ * `newText` change remaps (draft + queue) and advances the baseline. Files that
36
+ * leave the list drop their baseline so a future re-appearance re-baselines.
37
+ * @param opts - the sync inputs.
38
+ * @returns an unsubscribe function plus a direct remap verb for whole-file reverts.
39
+ */
40
+ export declare function attachReferenceRemap(opts: ReferenceRemapOpts): {
41
+ unsubscribe: () => void;
42
+ remapFile: (path: string, oldText: string, newText: string) => void;
43
+ };
44
+ export {};
@@ -45,3 +45,23 @@ export declare function setTabWidth(value: number): void;
45
45
  export declare function splitMode(): boolean;
46
46
  /** Persist the split-view preference. */
47
47
  export declare function setSplitMode(value: boolean): void;
48
+ /** Default quick-summon chord (toggle the review panel open/closed). */
49
+ export declare const DEFAULT_QUICK_SUMMON = "Ctrl+D";
50
+ /**
51
+ * The quick-summon chord. Stored as `Modifier+...+Key`; falls back to
52
+ * {@link DEFAULT_QUICK_SUMMON}.
53
+ * @returns the chord string.
54
+ */
55
+ export declare function quickSummonKey(): string;
56
+ /** Persist the quick-summon chord. */
57
+ export declare function setQuickSummonKey(value: string): void;
58
+ /**
59
+ * Whether a keyboard event matches a chord string like `Ctrl+D`. Modifier
60
+ * names are matched case-insensitively (`Ctrl`/`Control`, `Alt`/`Option`,
61
+ * `Shift`, `Meta`/`Cmd`/`Command`/`Win`); the final part is the key. Exact
62
+ * modifier set is required (extra modifiers do not match).
63
+ * @param event - the keydown event.
64
+ * @param shortcut - the chord string.
65
+ * @returns whether the event matches.
66
+ */
67
+ export declare function matchesShortcut(event: KeyboardEvent, shortcut: string): boolean;
@@ -20,6 +20,9 @@ export interface PendingDiffSnapshot {
20
20
  * superseded the redo history; the panel surfaces it once (deferred if the
21
21
  * panel is closed) via a bottom-right notice. */
22
22
  redoCleared?: boolean;
23
+ /** Latched file id whose last block just resolved; the panel prompts once to
24
+ * remove-or-keep it, then acknowledges to clear the latch. */
25
+ justResolved?: string | undefined;
23
26
  }
24
27
  /** The injected face the panel component receives from the plugin body. */
25
28
  export interface PendingPanelFace {
@@ -49,4 +52,9 @@ export interface PendingPanelFace {
49
52
  onImportVcs: (sessionId: SessionId, includeUntracked: boolean) => Promise<VcsImportValue>;
50
53
  /** Acknowledge the redo-cleared notice so it is only surfaced once. */
51
54
  onAckRedoCleared: () => void;
55
+ /** Acknowledge the just-resolved prompt so it is only surfaced once. */
56
+ onAckJustResolved: () => void;
57
+ /** Collapse the DSH sidebar (no-op when already collapsed) before the modal
58
+ * opens or fullscreens, so an expanded sidebar can't overlap the modal. */
59
+ collapseSidebar: () => void;
52
60
  }
@@ -34,6 +34,8 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
34
34
  reset: () => void;
35
35
  /** Acknowledge a redo-cleared notice so the panel surfaces it only once. */
36
36
  clearRedoCleared: () => void;
37
+ /** Acknowledge the just-resolved prompt so it is only surfaced once. */
38
+ clearJustResolved: () => void;
37
39
  }
38
40
  /**
39
41
  * Create the store over the review-channel port.
@@ -103,4 +103,7 @@ export interface DiffApprovalActionValue {
103
103
  outcome: DiffApprovalActionOutcome;
104
104
  /** Entry id the undo/redo affected; absent for a no-op or a non-undo action. */
105
105
  id?: string | undefined;
106
+ /** Set when a block keep/revert just cleared the entry's last remaining
107
+ * block (the entry stays listed with no pending diff). */
108
+ resolved?: boolean | undefined;
106
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",