dsh-diff-approval 0.15.0 → 0.17.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
@@ -424,11 +424,9 @@ function defaultOpenPath(path, action) {
424
424
  /** Cap on one VCS command's runtime; scans (git status, svn status) can be slow
425
425
  * on large trees but must not hang the import. */
426
426
  const VCS_COMMAND_TIMEOUT_MS = 6e4;
427
- /** Blobs up to this size are read straight off the executor's buffered stdout
428
- * (the small-blob path, no temp file); larger blobs go through the checkout-index
429
- * temp-file path because the executor cap truncates its stdout. Kept below the
430
- * cap so the small path never truncates. */
431
- const GIT_BLOB_STDOUT_SAFE = 6e4;
427
+ /** Slack added to a blob's size when raising the per-command stdout budget
428
+ * (`stdoutMaxBytes`), so a baseline blob is never truncated at the boundary. */
429
+ const GIT_BLOB_STDOUT_SLACK = 1024;
432
430
  /** Quote one argument for a POSIX-ish shell, so paths with spaces or special
433
431
  * characters survive interpolation into VCS command lines. */
434
432
  function shq(value) {
@@ -469,12 +467,13 @@ function detectVcsRoot(start) {
469
467
  }
470
468
  }
471
469
  /** Run one command through the shell executor; a non-zero exit throws. */
472
- async function runShell(shell, command, workdir, signal) {
470
+ async function runShell(shell, command, workdir, signal, stdoutMaxBytes) {
473
471
  const spec = shell.resolve({
474
472
  command,
475
473
  workdir,
476
474
  timeoutMs: VCS_COMMAND_TIMEOUT_MS,
477
- signal
475
+ signal,
476
+ stdoutMaxBytes
478
477
  });
479
478
  const result = await shell.run(spec);
480
479
  if (result.exitCode !== 0) {
@@ -539,18 +538,7 @@ async function gitChanges(input) {
539
538
  } catch {
540
539
  size = 0;
541
540
  }
542
- if (Number.isFinite(size) && size > 0 && size <= GIT_BLOB_STDOUT_SAFE) oldText = await runShell(shell, `git show :0:${shq(rel)}`, root, signal);
543
- else {
544
- const tempRel = (await runShell(shell, `git checkout-index --temp --force -- ${shq(rel)}`, root, signal)).trim().split(/\s+/)[0] ?? "";
545
- if (tempRel !== "") {
546
- const tempFile = resolve(root, tempRel);
547
- try {
548
- oldText = await readText(tempFile) ?? "";
549
- } finally {
550
- await rm(tempFile, { force: true }).catch(() => {});
551
- }
552
- }
553
- }
541
+ if (Number.isFinite(size) && size > 0) oldText = await runShell(shell, `git show :0:${shq(rel)}`, root, signal, size + GIT_BLOB_STDOUT_SLACK);
554
542
  } catch {
555
543
  oldText = "";
556
544
  }
@@ -55,8 +55,9 @@ interface RowRange {
55
55
  /** Imperative surface the parent uses to drive block navigation from the
56
56
  * shared toolbar/keyboard in split mode (its own `focus` is private here). */
57
57
  export interface SplitDiffHandle {
58
- jump: (direction: -1 | 1) => void;
58
+ jump: (direction: -1 | 1, wrapGuard?: boolean) => void;
59
59
  openSearch: () => void;
60
+ searchNext: (direction: -1 | 1) => boolean;
60
61
  }
61
62
  /** The two-column (side-by-side) whole-file diff view. */
62
63
  export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
@@ -71,6 +72,8 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
71
72
  leadRows: number;
72
73
  onBlockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
73
74
  onBlockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
75
+ /** Notify the parent to toast a block-wrap boundary / single-block (Ctrl+Up/Down). */
76
+ onWrapToast: (text: string) => void;
74
77
  } & import("react").RefAttributes<SplitDiffHandle>>;
75
78
  /**
76
79
  * Reconstruct the plain text of the current selection so auto-wrap's visual
@@ -23,6 +23,7 @@ export interface ConversationFace {
23
23
  state: {
24
24
  getSnapshot(): {
25
25
  queue: readonly QueueMessageView[];
26
+ draft?: string;
26
27
  };
27
28
  };
28
29
  };
@@ -51,6 +52,14 @@ export declare function resolveConversation(ctx: ClientContext, sessionId: Sessi
51
52
  export interface ConversationAccess {
52
53
  /** Write the current session's composer draft. */
53
54
  writeDraft: (text: string) => void;
55
+ /**
56
+ * Append a suffix to the current session's draft (a copy reference), or
57
+ * replace it when the draft is empty. The existing draft is read from the
58
+ * input state's plain text — the composer surface is a contenteditable div,
59
+ * not a <textarea>, so reading a DOM `.value` would always be empty and
60
+ * silently replace an existing draft instead of appending.
61
+ */
62
+ appendDraft: (suffix: string) => void;
54
63
  /** Read the current session's still-queued messages (queued placement only). */
55
64
  readQueue: () => readonly QueueMessageView[];
56
65
  /** Apply an edit mutation to one queued message's full content. */
@@ -11,6 +11,11 @@ export declare const zh: {
11
11
  'panel.aria': string;
12
12
  'panel.stats': string;
13
13
  'panel.blockPosition': string;
14
+ 'panel.blockAtEnd': string;
15
+ 'panel.blockAtStart': string;
16
+ 'panel.blockSingle': string;
17
+ 'panel.viewDiff': string;
18
+ 'panel.fileNotPending': string;
14
19
  'panel.selectHint': string;
15
20
  'panel.searchPlaceholder': string;
16
21
  'panel.missing': string;
@@ -30,6 +35,18 @@ export declare const zh: {
30
35
  'panel.quickSummon': string;
31
36
  'panel.quickSummonDesc': string;
32
37
  'panel.recordShortcut': string;
38
+ 'settings.keybindings': string;
39
+ 'panel.keyDesc': string;
40
+ 'panel.key.jumpUp': string;
41
+ 'panel.key.jumpDown': string;
42
+ 'panel.key.copyRef': string;
43
+ 'panel.key.openSearch': string;
44
+ 'panel.key.searchNext': string;
45
+ 'panel.key.searchPrev': string;
46
+ 'panel.key.undo': string;
47
+ 'panel.key.redo': string;
48
+ 'panel.key.cycleNext': string;
49
+ 'panel.key.cyclePrev': string;
33
50
  'settings.tabLabel': string;
34
51
  'row.create': string;
35
52
  'row.failed': string;
@@ -48,6 +65,8 @@ export declare const zh: {
48
65
  'action.nextDiff': string;
49
66
  'action.showFileList': string;
50
67
  'action.hideFileList': string;
68
+ 'action.viewSplit': string;
69
+ 'action.viewUnified': string;
51
70
  'action.copyHint': string;
52
71
  'action.copied': string;
53
72
  'action.langAuto': string;
@@ -94,6 +113,11 @@ export declare const en: {
94
113
  'panel.aria': string;
95
114
  'panel.stats': string;
96
115
  'panel.blockPosition': string;
116
+ 'panel.blockAtEnd': string;
117
+ 'panel.blockAtStart': string;
118
+ 'panel.blockSingle': string;
119
+ 'panel.viewDiff': string;
120
+ 'panel.fileNotPending': string;
97
121
  'panel.selectHint': string;
98
122
  'panel.searchPlaceholder': string;
99
123
  'panel.missing': string;
@@ -113,6 +137,18 @@ export declare const en: {
113
137
  'panel.quickSummon': string;
114
138
  'panel.quickSummonDesc': string;
115
139
  'panel.recordShortcut': string;
140
+ 'settings.keybindings': string;
141
+ 'panel.keyDesc': string;
142
+ 'panel.key.jumpUp': string;
143
+ 'panel.key.jumpDown': string;
144
+ 'panel.key.copyRef': string;
145
+ 'panel.key.openSearch': string;
146
+ 'panel.key.searchNext': string;
147
+ 'panel.key.searchPrev': string;
148
+ 'panel.key.undo': string;
149
+ 'panel.key.redo': string;
150
+ 'panel.key.cycleNext': string;
151
+ 'panel.key.cyclePrev': string;
116
152
  'settings.tabLabel': string;
117
153
  'row.create': string;
118
154
  'row.failed': string;
@@ -131,6 +167,8 @@ export declare const en: {
131
167
  'action.nextDiff': string;
132
168
  'action.showFileList': string;
133
169
  'action.hideFileList': string;
170
+ 'action.viewSplit': string;
171
+ 'action.viewUnified': string;
134
172
  'action.copyHint': string;
135
173
  'action.copied': string;
136
174
  'action.langAuto': string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Inject a "查看差异" button beside each DSH produced-file chip.
3
+ *
4
+ * The harness's `ProducedFiles` component renders each produced file as a
5
+ * `<button>` chip inside `[data-produced-files-row]`; the full path rides the
6
+ * chip's `title`. This plugin wants a quick "view this file's diff" affordance
7
+ * there, but the plugin should not have to touch the harness component. So this
8
+ * module watches the DOM for those chips (via a MutationObserver, the same
9
+ * bridge the dsh-pocket mobile fork uses to inject its 复制 buttons) and injects
10
+ * a small button after each one. Clicking it dispatches a window event that the
11
+ * diff-approval panel listens for (see PendingPanel), which opens the panel and
12
+ * selects the file when it is still pending, or toasts otherwise.
13
+ *
14
+ * The injected button is intentionally self-contained (inline styles + a fixed
15
+ * label) so it needs no stylesheet and no harness change.
16
+ */
17
+ /** Window event dispatched by the injected button; PendingPanel listens for it. */
18
+ export declare const OPEN_FILE_EVENT = "diff-approval:open-file";
19
+ /**
20
+ * Start injecting diff buttons into the produced-files row.
21
+ * @param label - localized "查看差异" text, used as the icon button's accessible
22
+ * name + hover title (the button renders only an icon).
23
+ * @param openPath - called with a produced-file path when its injected button is
24
+ * clicked; the diff-approval panel decides whether that file is still pending.
25
+ * @returns a cleanup that disconnects the observer and removes injected buttons.
26
+ */
27
+ export declare function startProducedDiffInjection(label: string, openPath: (path: string) => void): () => void;
@@ -31,6 +31,17 @@ export declare function referencePathOf(path: string, workspacePath: string | un
31
31
  * @returns the range label.
32
32
  */
33
33
  export declare function lineRangeLabel(start: number, end: number): string;
34
+ /**
35
+ * Build the bare reference label for a selected line range — `path:range` with
36
+ * no surrounding parentheses. Used for display (the status-bar copy control
37
+ * shows the reference without the token-wrapping parens).
38
+ * @param path - the selected file's path.
39
+ * @param workspacePath - the current workspace root, or `undefined`.
40
+ * @param start - first selected line number.
41
+ * @param end - last selected line number.
42
+ * @returns the `path:range` reference label.
43
+ */
44
+ export declare function referenceLabelOf(path: string, workspacePath: string | undefined, start: number, end: number): string;
34
45
  /**
35
46
  * Build the clipboard text for a selected line range, wrapped in parentheses so
36
47
  * the reference reads as one unambiguous token (and can be matched precisely).
@@ -68,6 +68,13 @@ export declare const DEFAULT_QUICK_SUMMON = "Ctrl+D";
68
68
  export declare function quickSummonKey(): string;
69
69
  /** Persist the quick-summon chord. */
70
70
  export declare function setQuickSummonKey(value: string): void;
71
+ /** Default chord for each configurable action (every supported key except the
72
+ * panel's own ESC-to-close, which is intentionally not remapped). */
73
+ export declare const DEFAULT_KEYBINDINGS: Record<string, string>;
74
+ /** The currently configured chord for one action; falls back to its default. */
75
+ export declare function keybindingOf(action: string): string;
76
+ /** Persist one action's chord. */
77
+ export declare function setKeybinding(action: string, chord: string): void;
71
78
  /**
72
79
  * Whether a keyboard event matches a chord string like `Ctrl+D`. Modifier
73
80
  * names are matched case-insensitively (`Ctrl`/`Control`, `Alt`/`Option`,
@@ -46,6 +46,15 @@ export interface WholeFileDiff {
46
46
  * @returns the normalized line bodies.
47
47
  */
48
48
  export declare function contentKey(text: string): string;
49
+ /** Reorder each contiguous change run (a maximal run of non-context rows) into
50
+ * the standard unified-diff shape — all `del` rows first, then all `add` rows.
51
+ * The diff package's Myers scan can emit per-line replacements (`del add del
52
+ * add`) for some content, which reads as interleaved in the viewer and makes
53
+ * the split view pair each line separately. Reordering a run to del-block→add-
54
+ * block keeps each row's own line numbers and never changes the run's row
55
+ * boundaries, so change blocks, split alignment, and keep/revert ranges stay
56
+ * correct — it only tidies the display order. */
57
+ export declare function normalizeChangeRuns(rows: WholeFileDiffRow[]): void;
49
58
  export declare function computeWholeFileDiff(oldText: string, newText: string): WholeFileDiff;
50
59
  /**
51
60
  * One intra-line segment of a changed line, used to highlight which characters
@@ -44,6 +44,8 @@ export interface ShellExecutorLike {
44
44
  workdir?: string | undefined;
45
45
  timeoutMs?: number | undefined;
46
46
  signal?: AbortSignal | undefined;
47
+ /** Foreground stdout capture budget in bytes; absent uses the executor's cap. */
48
+ stdoutMaxBytes?: number | undefined;
47
49
  }): unknown;
48
50
  run(spec: unknown): Promise<{
49
51
  exitCode: number | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.15.0",
3
+ "version": "0.17.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",