dsh-diff-approval 0.19.3 → 0.20.1

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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The add-path dialog: pick one path in a lazily-loaded workspace tree — or type
3
+ * it outright — then press Add. Nothing is added by pointing at it: the path and
4
+ * the no-change box are prepared first, and the host judges them once the button
5
+ * is pressed.
6
+ *
7
+ * The path box is the single source of truth for the selection: a tree row is
8
+ * highlighted while its path is the box's text, and editing the text by hand
9
+ * simply leaves no row highlighted.
10
+ *
11
+ * The dialog is a modal inside the panel: it owns Escape while it is on screen
12
+ * (the panel's own chords stand down, see `pathPickerOpen`).
13
+ * @module dsh-diff-approval/client/PathPicker
14
+ */
15
+ import type { ReactElement } from 'react';
16
+ import type { DiffApprovalAddValue, DiffApprovalBrowseEntry } from '../types.ts';
17
+ import type { Translator } from './locales.ts';
18
+ /** Whether this panel's add-path dialog is on screen. The panel's global chords
19
+ * consult this so the modal owns the keyboard while it is open. */
20
+ export declare function pathPickerOpen(): boolean;
21
+ /** Full props of the add-path dialog. */
22
+ export interface PathPickerProps {
23
+ /** The session's workspace root (absolute), for the tree's top row. */
24
+ rootPath?: string | undefined;
25
+ /** List one workspace directory level (absolute path; undefined is the root). */
26
+ onBrowse: (path?: string) => Promise<{
27
+ path: string;
28
+ entries: DiffApprovalBrowseEntry[];
29
+ truncated: boolean;
30
+ }>;
31
+ /** Add the chosen path; resolves to what the host's scan did. */
32
+ onAdd: (path: string, includeUnchanged: boolean) => Promise<DiffApprovalAddValue>;
33
+ /** Transient banner for an outcome the dialog does not stay open for. */
34
+ onToast: (text: string) => void;
35
+ onClose: () => void;
36
+ t: Translator;
37
+ }
38
+ /**
39
+ * The add-path dialog.
40
+ * @param props - the workspace root, the host calls, and the close/toast sinks.
41
+ * @returns the modal (rendered only while open by its parent).
42
+ */
43
+ export declare function PathPicker({ rootPath, onBrowse, onAdd, onToast, onClose, t }: PathPickerProps): ReactElement;
@@ -3,10 +3,11 @@ import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
3
3
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
4
4
  import type { DiffApprovalBlockRange, PendingFileDiff } from '../types.ts';
5
5
  import type { PendingPanelFace } from './slots.ts';
6
- import type { DiffApprovalKey } from './locales.ts';
6
+ import type { Translator } from './locales.ts';
7
7
  import { computeWholeFileDiff } from './whole-file-diff.ts';
8
- import type { IntraRun } from './whole-file-diff.ts';
9
- import { highlightLines } from './highlight.ts';
8
+ import type { ChangeBlock, IntraRun } from './whole-file-diff.ts';
9
+ import type { HighlightSides } from './highlight.ts';
10
+ import type { VisibleLines } from './windowed-highlight.ts';
10
11
  /** Normalize a path for comparison: forward slashes, no trailing slash. */
11
12
  export declare function normalizeDiffPath(p: string): string;
12
13
  /** Whether a produced-file chip path and a pending file path refer to the same
@@ -23,8 +24,6 @@ export declare const SIDEBAR_AUTO_COLLAPSE_PX = 1024;
23
24
  export declare function wrapInto(text: string, widthPx: number, measure: (t: string) => number, tabPx: number): string[];
24
25
  /** Full panel props composed by the sidebar footer-action slot. */
25
26
  export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<PendingPanelFace> & PropsLocale<'diff-approval'>;
26
- /** Locale translator used by the panel and its rows. */
27
- type Translator = (key: DiffApprovalKey, params?: Record<string, unknown>) => string;
28
27
  /**
29
28
  * Open the DSH settings dialog and switch to this plugin's section. The
30
29
  * settings shell keeps its open state and the active section id as
@@ -43,16 +42,6 @@ interface RowModel {
43
42
  /** Intra-line runs keyed by row index, present only for annotated del/add rows. */
44
43
  intra: Map<number, IntraRun[]>;
45
44
  }
46
- /** One file's deferred syntax-highlight runs, one entry per side. */
47
- interface HighlightRuns {
48
- oldRuns: ReturnType<typeof highlightLines>;
49
- newRuns: ReturnType<typeof highlightLines>;
50
- }
51
- /** One contiguous run of changed rows, treated as a single modification. */
52
- interface ChangeBlock {
53
- start: number;
54
- end: number;
55
- }
56
45
  /** One selected line range in row indices, normalized low-to-high. */
57
46
  interface RowRange {
58
47
  start: number;
@@ -76,7 +65,7 @@ export interface SplitDiffHandle {
76
65
  export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
77
66
  file: PendingFileDiff;
78
67
  model: RowModel;
79
- runs: HighlightRuns | undefined;
68
+ runs: HighlightSides | undefined;
80
69
  langWrap: boolean;
81
70
  tabWidthSpaces: number;
82
71
  busy: boolean;
@@ -87,6 +76,9 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
87
76
  onBlockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
88
77
  /** Notify the parent to toast a block-wrap boundary / single-block (Ctrl+Up/Down). */
89
78
  onWrapToast: (text: string) => void;
79
+ /** Report which source lines this view is showing, so the parent's windowed
80
+ * highlighter follows this view's own scroller (it has its own virtual window). */
81
+ onVisibleLines: (visible: VisibleLines) => void;
90
82
  } & import("react").RefAttributes<SplitDiffHandle>>;
91
83
  /**
92
84
  * Reconstruct the plain text of the current selection so auto-wrap's visual
@@ -97,5 +89,5 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
97
89
  */
98
90
  export declare function selectedPlainText(): string | undefined;
99
91
  /** Render the pending-edit review panel and its unified footer action. */
100
- export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPreviewImage, onPasteReference, onUndo, onRedo, onImportVcs, onRefreshVcs, onKeepAll, onRevertAll, onAckRedoCleared, collapseSidebar, t, }: PendingPanelProps): import("react").JSX.Element;
92
+ export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPreviewImage, onPasteReference, onUndo, onRedo, onImportVcs, onRefreshVcs, onBrowse, onAddPath, onKeepAll, onRevertAll, onAckRedoCleared, collapseSidebar, t, }: PendingPanelProps): import("react").JSX.Element;
101
93
  export {};
@@ -10,11 +10,42 @@
10
10
  * @module dsh-diff-approval/client/highlight
11
11
  */
12
12
  import type { CSSProperties } from 'react';
13
+ import type { HighlighterCore } from 'shiki/core';
13
14
  /** One highlighted run of a line: literal text plus a color style. */
14
15
  export interface HighlightSpan {
15
16
  text: string;
16
17
  style: CSSProperties;
17
18
  }
19
+ /** One file's highlight runs, one entry per side and line (index = line - 1). A
20
+ * hole means that line has not been highlighted — the viewer renders it plain,
21
+ * which is the honest state of a windowed highlighter: only what has been looked
22
+ * at is tokenized. */
23
+ export interface HighlightSides {
24
+ oldRuns: HighlightSpan[][];
25
+ newRuns: HighlightSpan[][];
26
+ }
27
+ /** A saved grammar state: resuming from one continues tokenization *exactly*
28
+ * where it stopped, instead of assuming the window's first line starts the file.
29
+ * Opaque to callers — only `highlightWindow` produces and consumes it, and
30
+ * `undefined` means "no saved state" (the file's first line is top-level). */
31
+ export type HighlightState = Parameters<HighlighterCore['codeToTokens']>[1]['grammarState'];
32
+ /** One highlighted window: the requested lines' runs and the grammar state after
33
+ * the last of them, for an exact continuation by the next window. */
34
+ export interface HighlightWindow {
35
+ runs: HighlightSpan[][];
36
+ state: HighlightState;
37
+ }
38
+ /** Options for {@link highlightWindow}. */
39
+ export interface HighlightWindowOptions {
40
+ /** Lines of preceding context to run the grammar over without returning them,
41
+ * so a window starting inside a multi-line construct (a block comment, a
42
+ * template literal, a fenced code block) still colours correctly. Ignored when
43
+ * `state` is given: that one is exact. */
44
+ context?: number;
45
+ /** An exact state saved at this window's first line (a previous window's
46
+ * `state`). */
47
+ state?: HighlightState;
48
+ }
18
49
  /**
19
50
  * Primary grammar ids offered in the viewer's language selector, in picker
20
51
  * order (alphabetical). Kept explicit instead of deriving from `LANGS`: some
@@ -28,13 +59,25 @@ export declare const HIGHLIGHT_LANGS: string[];
28
59
  /** Conventional display name for a grammar id, falling back to the id itself. */
29
60
  export declare function languageDisplayName(id: string): string;
30
61
  /**
31
- * Tokenize `code` into per-line highlighted runs when `lang` names a
32
- * registered grammar; `undefined` means the caller renders its plain fallback.
33
- * Each run's color is a `--shiki-*` custom property, keeping token colors on
34
- * the harness theme's sheets. The trailing newline shiki appends as a final
35
- * empty line is dropped so the run count matches the caller's own line array.
36
- * @param code - the source text.
62
+ * Tokenize the lines `[from, to)` into per-line highlighted runs when `lang`
63
+ * names a registered grammar; `undefined` means the caller renders its plain
64
+ * fallback. Each run's color is a `--shiki-*` custom property, keeping token
65
+ * colors on the harness theme's sheets. The trailing newline shiki appends as a
66
+ * final empty line is dropped so the run count matches the caller's own array.
67
+ *
68
+ * This is the viewer's only entry point, and it is deliberately windowed: the
69
+ * code view renders a virtual window of rows, so highlighting a whole file to
70
+ * show one screenful is almost all waste (measured on a 3818-line file: ~945 ms
71
+ * for both sides whole-file against ~8 ms for one window). A window is continued
72
+ * *exactly* from a previous one's `state`, or approximately from `context` lines
73
+ * when no state is known yet — which is what lets a jump straight to the middle
74
+ * of a file colour correctly without tokenizing anything above it.
75
+ * @param lines - the side's lines, indexed from 0 as the caller's line numbers are.
37
76
  * @param lang - the Shiki grammar id, or `undefined` for plain text.
38
- * @returns one entry per source line (each an array of runs), or `undefined` when unhighlightable.
77
+ * @param from - the first line index to highlight (inclusive, clamped).
78
+ * @param to - one past the last line index to highlight (clamped).
79
+ * @param options - the grammar state to resume from and/or context lines for the grammar.
80
+ * @returns the window's runs and its end state, or `undefined` when the language
81
+ * is unknown, the range is empty, or the window's own text is too large.
39
82
  */
40
- export declare function highlightLines(code: string, lang: string | undefined): HighlightSpan[][] | undefined;
83
+ export declare function highlightWindow(lines: readonly string[], lang: string | undefined, from: number, to: number, options?: HighlightWindowOptions): HighlightWindow | undefined;
@@ -10,3 +10,11 @@
10
10
  * @returns the language id, or `undefined` when unknown.
11
11
  */
12
12
  export declare function langFromPath(path: string): string | undefined;
13
+ /**
14
+ * The lowercase extension of a file path, without the dot — the key a manual
15
+ * language choice is remembered under. A name with no dot has no suffix, so a
16
+ * per-suffix preference never leaks onto every extension-less file at once.
17
+ * @param path - the file path, any separator style.
18
+ * @returns the suffix, or `undefined` when the name carries none.
19
+ */
20
+ export declare function suffixOfPath(path: string): string | undefined;
@@ -130,9 +130,32 @@ export declare const zh: {
130
130
  'panel.refreshNone': string;
131
131
  'panel.refreshUntrackedHint': string;
132
132
  'panel.refreshFailed': string;
133
+ 'action.addPath': string;
134
+ 'panel.addTitle': string;
135
+ 'panel.addPathPlaceholder': string;
136
+ 'panel.addPathGo': string;
137
+ 'panel.addHint': string;
138
+ 'panel.addIncludeUnchanged': string;
139
+ 'panel.addRoot': string;
140
+ 'panel.addTruncated': string;
141
+ 'panel.addDone': string;
142
+ 'panel.addDoneTruncated': string;
143
+ 'panel.addDuplicate': string;
144
+ 'panel.addUnchanged': string;
145
+ 'panel.addEmpty': string;
146
+ 'panel.addNoEntries': string;
147
+ 'panel.addMissing': string;
148
+ 'panel.addOutside': string;
149
+ 'panel.addFailed': string;
150
+ 'action.cancel': string;
151
+ 'action.collapseRow': string;
152
+ 'action.expandRow': string;
133
153
  };
134
154
  /** Translation keys owned by the pending-edit review namespace. */
135
155
  export type DiffApprovalKey = keyof typeof zh;
156
+ /** Locale translator for this plugin's namespace: the panel's and the settings
157
+ * section's `t`, typed against the dictionary keys. */
158
+ export type Translator = (key: DiffApprovalKey, params?: Record<string, unknown>) => string;
136
159
  declare module '@deepseek-ai/dsh-client-ui-slots' {
137
160
  interface LocaleNamespaceMap {
138
161
  /** Pending-edit review UI copy. */
@@ -269,4 +292,24 @@ export declare const en: {
269
292
  'panel.refreshNone': string;
270
293
  'panel.refreshUntrackedHint': string;
271
294
  'panel.refreshFailed': string;
295
+ 'action.addPath': string;
296
+ 'panel.addTitle': string;
297
+ 'panel.addPathPlaceholder': string;
298
+ 'panel.addPathGo': string;
299
+ 'panel.addHint': string;
300
+ 'panel.addIncludeUnchanged': string;
301
+ 'panel.addRoot': string;
302
+ 'panel.addTruncated': string;
303
+ 'panel.addDone': string;
304
+ 'panel.addDoneTruncated': string;
305
+ 'panel.addDuplicate': string;
306
+ 'panel.addUnchanged': string;
307
+ 'panel.addEmpty': string;
308
+ 'panel.addNoEntries': string;
309
+ 'panel.addMissing': string;
310
+ 'panel.addOutside': string;
311
+ 'panel.addFailed': string;
312
+ 'action.cancel': string;
313
+ 'action.collapseRow': string;
314
+ 'action.expandRow': string;
272
315
  };
@@ -5,7 +5,7 @@
5
5
  * @module dsh-diff-approval/client/port
6
6
  */
7
7
  import type { ClientConnectionRpc, SessionId } from '@deepseek-ai/dsh-client-connection/client';
8
- import type { DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBulkValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalPreviewImageValue, DiffApprovalRefreshValue, VcsImportValue } from '../types.ts';
8
+ import type { DiffApprovalActionValue, DiffApprovalAddValue, DiffApprovalBlockRange, DiffApprovalBrowseValue, DiffApprovalBulkValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalPreviewImageValue, DiffApprovalRefreshValue, VcsImportValue } from '../types.ts';
9
9
  /** The channel the host half registers and this port calls. */
10
10
  export declare const DIFF_APPROVAL_CHANNEL = "/diff-approval";
11
11
  /** This package's business verbs over the review channel. */
@@ -28,6 +28,10 @@ export interface DiffApprovalPort {
28
28
  importVcs(sessionId: SessionId, includeUntracked: boolean): Promise<VcsImportValue>;
29
29
  /** Replace one entry's diff with the file's current local VCS change. */
30
30
  refreshVcs(sessionId: SessionId, id: string, includeUntracked: boolean): Promise<DiffApprovalRefreshValue>;
31
+ /** List one workspace directory level (workspace-relative; `''` is the root). */
32
+ browse(sessionId: SessionId, path?: string): Promise<DiffApprovalBrowseValue>;
33
+ /** Add one named path (a file, or a directory's whole subtree) to the list. */
34
+ addPath(sessionId: SessionId, path: string, includeUnchanged: boolean): Promise<DiffApprovalAddValue>;
31
35
  /** Open one file with its default application or reveal it in the folder. */
32
36
  open(sessionId: SessionId, id: string, action: DiffApprovalOpenAction): Promise<DiffApprovalOpenValue>;
33
37
  /** Keep every pending entry of one session in a single host call (one batch). */
@@ -108,6 +108,22 @@ export declare function setDiffDelColor(value: string): void;
108
108
  export declare function currentDiffAddColor(): string;
109
109
  /** The theme's removed-line base color, for the settings swatch default. */
110
110
  export declare function currentDiffDelColor(): string;
111
+ /**
112
+ * The highlight language chosen by hand for one file suffix, remembered so the
113
+ * choice sticks for every file with that suffix. Not a Settings row: it is a
114
+ * per-suffix consequence of using the language picker, so it lives here as
115
+ * storage only.
116
+ * @param suffix - the lowercase extension without the dot (see `suffixOfPath`).
117
+ * @returns the remembered language id, or undefined for "auto".
118
+ */
119
+ export declare function languageForSuffix(suffix: string): string | undefined;
120
+ /**
121
+ * Remember (or clear) the language chosen for one suffix. A `null` language
122
+ * forgets the suffix, which is what picking "auto" means.
123
+ * @param suffix - the lowercase extension without the dot.
124
+ * @param language - the language id, or null to forget the suffix.
125
+ */
126
+ export declare function setLanguageForSuffix(suffix: string, language: string | null): void;
111
127
  /**
112
128
  * Whether the whole-file diff view uses the two-column (side-by-side) layout.
113
129
  * Default off (single column): the unified diff. Only an explicit `'1'` enables
@@ -1,7 +1,7 @@
1
1
  /** The panel's injected business face and its observable snapshot. */
2
2
  import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
3
3
  import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
4
- import type { DiffApprovalBlockRange, DiffApprovalOpenAction, DiffApprovalRefreshValue, PendingFileDiff, VcsImportValue } from '../types.ts';
4
+ import type { DiffApprovalAddValue, DiffApprovalBlockRange, DiffApprovalBrowseValue, DiffApprovalOpenAction, DiffApprovalRefreshValue, PendingFileDiff, VcsImportValue } from '../types.ts';
5
5
  /** What the panel reads and drives: the pending list plus in-flight entries. */
6
6
  export interface PendingDiffSnapshot {
7
7
  /** Whether a list read has completed at least once. */
@@ -54,6 +54,10 @@ export interface PendingPanelFace {
54
54
  /** Replace one entry's diff with the file's current local VCS change, then
55
55
  * refresh the list; resolves to what the scan found. */
56
56
  onRefreshVcs: (sessionId: SessionId, id: string, includeUntracked: boolean) => Promise<DiffApprovalRefreshValue>;
57
+ /** List one workspace directory level for the add-path dialog. */
58
+ onBrowse: (sessionId: SessionId, path?: string) => Promise<DiffApprovalBrowseValue>;
59
+ /** Add one named path (a file, or a directory's whole subtree) to the list. */
60
+ onAddPath: (sessionId: SessionId, path: string, includeUnchanged: boolean) => Promise<DiffApprovalAddValue>;
57
61
  /** Keep every pending entry of one session in a single host call (bulk). */
58
62
  onKeepAll: (sessionId: SessionId) => Promise<void>;
59
63
  /** Revert every pending entry of one session in a single host call (bulk). */
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
9
9
  import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
10
- import type { DiffApprovalBlockRange, DiffApprovalOpenAction, DiffApprovalRefreshValue, VcsImportValue } from '../types.ts';
10
+ import type { DiffApprovalAddValue, DiffApprovalBlockRange, DiffApprovalBrowseValue, DiffApprovalOpenAction, DiffApprovalRefreshValue, VcsImportValue } from '../types.ts';
11
11
  import type { PendingDiffSnapshot } from './slots.ts';
12
12
  import type { DiffApprovalPort } from './port.ts';
13
13
  /** The observable the panel reads and the plugin body drives. */
@@ -31,6 +31,10 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
31
31
  /** Replace one entry's diff with the file's current local VCS change, then
32
32
  * refresh; resolves to what the scan found. */
33
33
  refreshVcs: (sessionId: SessionId, id: string, includeUntracked: boolean) => Promise<DiffApprovalRefreshValue>;
34
+ /** List one workspace directory level for the add-path dialog. */
35
+ browse: (sessionId: SessionId, path?: string) => Promise<DiffApprovalBrowseValue>;
36
+ /** Add one named path to the list, then refresh; resolves to what the scan did. */
37
+ addPath: (sessionId: SessionId, path: string, includeUnchanged: boolean) => Promise<DiffApprovalAddValue>;
34
38
  /** Open one file with its default application or reveal it in the folder. */
35
39
  open: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
36
40
  /** Keep every pending entry of one session in a single host call, then refresh. */
@@ -5,6 +5,7 @@
5
5
  * derivation; the panel owns rendering. The `diff` package is browser-safe.
6
6
  * @module dsh-diff-approval/client/whole-file-diff
7
7
  */
8
+ import type { DiffApprovalBlockRange } from '../types.ts';
8
9
  /** One rendered body line of the whole-file view. */
9
10
  export interface WholeFileDiffRow {
10
11
  /** `context` lines are unchanged, `del`/`add` mark the removed/added sides. */
@@ -25,6 +26,26 @@ export interface WholeFileDiff {
25
26
  /** Number of added lines. */
26
27
  added: number;
27
28
  }
29
+ /** One contiguous run of changed rows, treated as a single modification. */
30
+ export interface ChangeBlock {
31
+ start: number;
32
+ end: number;
33
+ }
34
+ /** Split a row list into maximal runs of non-context rows. */
35
+ export declare function changeBlocksOf(diff: WholeFileDiff): ChangeBlock[];
36
+ /**
37
+ * One diff block's old/new line ranges, 1-based inclusive, for block-level
38
+ * keep/revert. A side with no lines (a pure addition or deletion) is empty; its
39
+ * start is that side's insertion point — the line after the surrounding context
40
+ * — so the host can insert there.
41
+ *
42
+ * Shared by the source view and the Markdown preview: both derive blocks from
43
+ * the same rows, so a preview element and a source row name the same target.
44
+ * @param rows - the whole-file diff rows.
45
+ * @param block - the block's row range.
46
+ * @returns the old and new line ranges.
47
+ */
48
+ export declare function blockRangesOf(rows: readonly WholeFileDiffRow[], block: ChangeBlock): DiffApprovalBlockRange;
28
49
  /**
29
50
  * Compute the whole-file view between two contents. The context budget is the
30
51
  * side lengths, so every hunk covers the file and unchanged lines survive as
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Windowed syntax highlighting for the diff viewer.
3
+ *
4
+ * The code view renders a virtual window of rows, so the viewer only ever needs
5
+ * the colors of the lines near the viewport. Highlighting a whole file to show
6
+ * one screenful is almost all waste — measured on a 3818-line file, the two
7
+ * sides whole-file cost ~945 ms of main-thread time against ~8 ms for one window
8
+ * — so this hook keeps a per-side store of highlighted lines and fills it:
9
+ *
10
+ * 1. **The window first.** Whenever the visible line range changes, the lines
11
+ * that are missing are highlighted (with a margin either side, debounced, so a
12
+ * fast scroll computes the window it lands on instead of every window it
13
+ * passes). A window with no state yet gets `context` lines above it so a
14
+ * construct that started earlier still colours correctly.
15
+ * 2. **Already-highlighted lines are kept.** Scrolling back, or nudging the
16
+ * window by a few rows, finds the lines present and does no work at all.
17
+ * 3. **An idle backfill** walks the file top-down in chunks when the pane reports
18
+ * a real viewport, recording the exact grammar state at each chunk boundary.
19
+ * Windows inside a backfilled region then resume exactly (no context guess),
20
+ * and a random scroll position is coloured before the user gets there.
21
+ *
22
+ * Lines that have never been highlighted are holes in the returned arrays: the
23
+ * viewer renders them plain, which is the honest state of the work done so far.
24
+ * @module dsh-diff-approval/client/windowed-highlight
25
+ */
26
+ import type { HighlightSides } from './highlight.ts';
27
+ /** One visible window, in source lines (1-based, inclusive). */
28
+ export interface LineRange {
29
+ from: number;
30
+ to: number;
31
+ }
32
+ /** The source lines one view is showing, plus whether that view measured a real
33
+ * viewport. Reported by whichever view is mounted (the single column computes its
34
+ * own window; the split view reports its own, which is the only one that matches
35
+ * its scroll container). */
36
+ export interface VisibleLines {
37
+ oldRange: LineRange | undefined;
38
+ newRange: LineRange | undefined;
39
+ live: boolean;
40
+ }
41
+ /** What the hook needs to know about the file and the pane. */
42
+ export interface WindowedHighlightInput {
43
+ /** Identity of the content and language: a change resets everything. */
44
+ key: unknown;
45
+ /** The old side's lines (index 0 = line 1). */
46
+ oldLines: readonly string[];
47
+ /** The new side's lines (index 0 = line 1). */
48
+ newLines: readonly string[];
49
+ /** The Shiki grammar id, or undefined for plain text. */
50
+ lang: string | undefined;
51
+ /** The visible source lines of the old side, if any are on screen. */
52
+ oldRange: LineRange | undefined;
53
+ /** The visible source lines of the new side, if any are on screen. */
54
+ newRange: LineRange | undefined;
55
+ /** Whether the pane reports a real viewport. The idle backfill only runs with
56
+ * one: without a viewport (a test DOM, a pane that has not measured yet) there
57
+ * is no "idle" to spend and nothing to prefetch for. */
58
+ live: boolean;
59
+ }
60
+ /**
61
+ * Keep syntax highlighting for the pane's visible window, plus (with a live
62
+ * viewport) an idle top-down backfill of the rest.
63
+ * @param input - the file's lines, language, and the visible line ranges.
64
+ * @returns the runs per side, indexed by line - 1, with holes for unhighlighted lines.
65
+ */
66
+ export declare function useWindowedHighlight(input: WindowedHighlightInput): HighlightSides;
@@ -74,6 +74,16 @@ export declare class PendingDiffStore {
74
74
  * @returns whether the store changed.
75
75
  */
76
76
  restore(entry: PendingEntry): boolean;
77
+ /**
78
+ * Admit one entry into the list without the change guard {@link fold} applies.
79
+ * A listed entry may carry no diff at all: a path the user added by hand has
80
+ * no local change until one appears, and a fully-resolved file has already
81
+ * folded its diff away. This is the guard-free put {@link restore} performs,
82
+ * named for its other caller.
83
+ * @param entry - the entry to insert or replace by path.
84
+ * @returns whether the store changed.
85
+ */
86
+ insert(entry: PendingEntry): boolean;
77
87
  /**
78
88
  * Merge persisted entries into the store, one per path after folding. A live
79
89
  * entry wins over a persisted one only when its time is newer (folders are
@@ -149,3 +149,54 @@ export interface DiffApprovalActionValue {
149
149
  * block (the entry stays listed with no pending diff). */
150
150
  resolved?: boolean | undefined;
151
151
  }
152
+ /** One row of a browsed directory level. */
153
+ export interface DiffApprovalBrowseEntry {
154
+ /** Base name inside the listed directory. */
155
+ name: string;
156
+ /** What the child is; `other` covers anything neither file nor directory. */
157
+ type: 'file' | 'directory' | 'other';
158
+ /** Absolute host path — the same value `add-path` takes, so the panel can show
159
+ * (and add) exactly what a row names. */
160
+ path: string;
161
+ /** Byte size, present only for a regular file the backend reports. */
162
+ size?: number | undefined;
163
+ }
164
+ /** Value returned by the channel's list-path endpoint: one directory level. */
165
+ export interface DiffApprovalBrowseValue {
166
+ /** The listed directory's absolute path. */
167
+ path: string;
168
+ /** Direct children: directories first, then files, each name-sorted. */
169
+ entries: DiffApprovalBrowseEntry[];
170
+ /** The response hit the entry cap, so children are missing from `entries`. */
171
+ truncated: boolean;
172
+ }
173
+ /** What one hand-added path did. */
174
+ export type DiffApprovalAddOutcome =
175
+ /** At least one entry landed in the list (a directory can add many). */
176
+ 'added'
177
+ /** Everything scanned was already listed; nothing changed. */
178
+ | 'duplicate'
179
+ /** The path has no local VCS change and the caller did not ask to add those. */
180
+ | 'unchanged'
181
+ /** A directory scan found nothing to add and nothing was listed already. */
182
+ | 'empty'
183
+ /** The path does not exist (or is neither a file nor a directory). */
184
+ | 'missing'
185
+ /** The path lies outside the session's workspace. */
186
+ | 'outside'
187
+ /** The workspace is not inside a git/svn/p4 checkout. */
188
+ | 'no-vcs'
189
+ /** The scan itself failed; `message` carries the reason. */
190
+ | 'failed';
191
+ /** Value returned by the channel's add-path endpoint. */
192
+ export interface DiffApprovalAddValue {
193
+ outcome: DiffApprovalAddOutcome;
194
+ /** How many entries the add put into the list. */
195
+ added: number;
196
+ /** How many scanned paths were already listed (left untouched). */
197
+ duplicates: number;
198
+ /** Whether the no-change walk hit its file cap, so some files were not added. */
199
+ truncated?: boolean | undefined;
200
+ /** Failure detail for `outcome: 'failed'`. */
201
+ message?: string | undefined;
202
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.19.3",
3
+ "version": "0.20.1",
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",
@@ -67,18 +67,6 @@
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@deepseek-ai/cordis": ">=4.0.1",
70
- "@deepseek-ai/dsh-client-connection": ">=0.1.0-rc.5",
71
- "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.5",
72
- "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.5",
73
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.5",
74
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.5",
75
- "@deepseek-ai/dsh-client-ui-sidebar": ">=0.1.0-rc.5",
76
- "@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.5",
77
- "@deepseek-ai/dsh-fs": ">=0.1.0-rc.5",
78
- "@deepseek-ai/dsh-host-apiproxy": ">=0.1.0-rc.5",
79
- "@deepseek-ai/dsh-session": ">=0.1.0-rc.5",
80
- "@deepseek-ai/dsh-tools": ">=0.1.0-rc.5",
81
- "@deepseek-ai/dsh-workspace": ">=0.1.0-rc.5",
82
70
  "react": "^18.2.0"
83
71
  },
84
72
  "devDependencies": {