dsh-diff-approval 0.19.2 → 0.20.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.
@@ -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;
@@ -66,13 +55,17 @@ interface RowRange {
66
55
  export interface SplitDiffHandle {
67
56
  jump: (direction: -1 | 1, wrapGuard?: boolean, singleToast?: boolean) => void;
68
57
  openSearch: () => void;
58
+ toggleSearch: () => void;
59
+ closeSearch: () => boolean;
69
60
  searchNext: (direction: -1 | 1) => boolean;
61
+ toggleMatchCase: () => boolean;
62
+ toggleMatchWholeWord: () => boolean;
70
63
  }
71
64
  /** The two-column (side-by-side) whole-file diff view. */
72
65
  export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
73
66
  file: PendingFileDiff;
74
67
  model: RowModel;
75
- runs: HighlightRuns | undefined;
68
+ runs: HighlightSides | undefined;
76
69
  langWrap: boolean;
77
70
  tabWidthSpaces: number;
78
71
  busy: boolean;
@@ -83,6 +76,9 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
83
76
  onBlockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
84
77
  /** Notify the parent to toast a block-wrap boundary / single-block (Ctrl+Up/Down). */
85
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;
86
82
  } & import("react").RefAttributes<SplitDiffHandle>>;
87
83
  /**
88
84
  * Reconstruct the plain text of the current selection so auto-wrap's visual
@@ -93,5 +89,5 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
93
89
  */
94
90
  export declare function selectedPlainText(): string | undefined;
95
91
  /** Render the pending-edit review panel and its unified footer action. */
96
- export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPreviewImage, onPasteReference, onUndo, onRedo, onImportVcs, 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;
97
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;
@@ -59,6 +59,8 @@ export declare const zh: {
59
59
  'panel.key.openSearch': string;
60
60
  'panel.key.searchNext': string;
61
61
  'panel.key.searchPrev': string;
62
+ 'panel.key.matchCase': string;
63
+ 'panel.key.matchWholeWord': string;
62
64
  'panel.key.undo': string;
63
65
  'panel.key.redo': string;
64
66
  'panel.key.cycleNext': string;
@@ -116,9 +118,44 @@ export declare const zh: {
116
118
  'status.missing': string;
117
119
  'panel.resolvedAsk': string;
118
120
  'panel.keepInList': string;
121
+ 'panel.fileKeptAsk': string;
122
+ 'panel.fileRevertedAsk': string;
123
+ 'panel.confirmFileRemove': string;
124
+ 'panel.confirmFileRemoveDesc': string;
125
+ 'action.refreshVcs': string;
126
+ 'action.matchCase': string;
127
+ 'action.matchWholeWord': string;
128
+ 'panel.refreshDone': string;
129
+ 'panel.refreshUnchanged': string;
130
+ 'panel.refreshNone': string;
131
+ 'panel.refreshUntrackedHint': string;
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;
119
153
  };
120
154
  /** Translation keys owned by the pending-edit review namespace. */
121
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;
122
159
  declare module '@deepseek-ai/dsh-client-ui-slots' {
123
160
  interface LocaleNamespaceMap {
124
161
  /** Pending-edit review UI copy. */
@@ -184,6 +221,8 @@ export declare const en: {
184
221
  'panel.key.openSearch': string;
185
222
  'panel.key.searchNext': string;
186
223
  'panel.key.searchPrev': string;
224
+ 'panel.key.matchCase': string;
225
+ 'panel.key.matchWholeWord': string;
187
226
  'panel.key.undo': string;
188
227
  'panel.key.redo': string;
189
228
  'panel.key.cycleNext': string;
@@ -241,4 +280,36 @@ export declare const en: {
241
280
  'status.missing': string;
242
281
  'panel.resolvedAsk': string;
243
282
  'panel.keepInList': string;
283
+ 'panel.fileKeptAsk': string;
284
+ 'panel.fileRevertedAsk': string;
285
+ 'panel.confirmFileRemove': string;
286
+ 'panel.confirmFileRemoveDesc': string;
287
+ 'action.refreshVcs': string;
288
+ 'action.matchCase': string;
289
+ 'action.matchWholeWord': string;
290
+ 'panel.refreshDone': string;
291
+ 'panel.refreshUnchanged': string;
292
+ 'panel.refreshNone': string;
293
+ 'panel.refreshUntrackedHint': string;
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;
244
315
  };
@@ -5,17 +5,17 @@
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, 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. */
12
12
  export interface DiffApprovalPort {
13
13
  /** Read one session's pending entries (plus its workspace root), oldest capture first. */
14
14
  list(sessionId: SessionId): Promise<DiffApprovalListValue>;
15
- /** Keep one operation. */
16
- keep(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
17
- /** Revert one operation. */
18
- revert(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
15
+ /** Keep one operation. `keepListed` leaves the resolved entry in the list. */
16
+ keep(sessionId: SessionId, id: string, keepListed?: boolean): Promise<DiffApprovalActionValue>;
17
+ /** Revert one operation. `keepListed` leaves the resolved entry in the list. */
18
+ revert(sessionId: SessionId, id: string, keepListed?: boolean): Promise<DiffApprovalActionValue>;
19
19
  /** Keep one diff block (accept its change into the tracked baseline). */
20
20
  blockKeep(sessionId: SessionId, id: string, block: DiffApprovalBlockRange, removeWhenResolved?: boolean): Promise<DiffApprovalActionValue>;
21
21
  /** Revert one diff block (restore its old lines in the file). */
@@ -26,6 +26,12 @@ export interface DiffApprovalPort {
26
26
  redo(sessionId: SessionId): Promise<DiffApprovalActionValue>;
27
27
  /** Import the workspace's local VCS changes as pending entries. */
28
28
  importVcs(sessionId: SessionId, includeUntracked: boolean): Promise<VcsImportValue>;
29
+ /** Replace one entry's diff with the file's current local VCS change. */
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>;
29
35
  /** Open one file with its default application or reveal it in the folder. */
30
36
  open(sessionId: SessionId, id: string, action: DiffApprovalOpenAction): Promise<DiffApprovalOpenValue>;
31
37
  /** Keep every pending entry of one session in a single host call (one batch). */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The in-file search matcher, shared by the unified diff and the split view so
3
+ * both highlight and count the same occurrences.
4
+ * @module dsh-diff-approval/client/search
5
+ */
6
+ /** Which occurrences an in-file search accepts. */
7
+ export interface SearchOptions {
8
+ /** Match the query's letter case exactly. */
9
+ caseSensitive: boolean;
10
+ /** Accept only whole words (no identifier character touching either end). */
11
+ wholeWord: boolean;
12
+ }
13
+ /** The plain search: case-insensitive, substring. */
14
+ export declare const DEFAULT_SEARCH_OPTIONS: SearchOptions;
15
+ /**
16
+ * Character ranges of every occurrence of `query` in `text` that the options
17
+ * accept. Case-insensitive and substring-based unless narrowed.
18
+ * @param text - the line to search.
19
+ * @param query - the search text; empty matches nothing.
20
+ * @param options - case and whole-word narrowing.
21
+ * @returns `[start, end)` ranges in ascending order.
22
+ */
23
+ export declare function matchRangesOf(text: string, query: string, options?: SearchOptions): [number, number][];
@@ -36,6 +36,31 @@ export declare function setPasteOnCopyEnabled(value: boolean): void;
36
36
  export declare function includeUntrackedEnabled(): boolean;
37
37
  /** Persist the import-untracked preference. */
38
38
  export declare function setIncludeUntrackedEnabled(value: boolean): void;
39
+ /**
40
+ * Whether a whole-file keep/revert asks before dropping the resolved file from
41
+ * the list. Defaults to on; only an explicit `'0'` disables it, which then
42
+ * removes the file straight away.
43
+ * @returns whether the remove prompt is enabled.
44
+ */
45
+ export declare function confirmFileRemoveEnabled(): boolean;
46
+ /** Persist the whole-file remove-prompt preference. */
47
+ export declare function setConfirmFileRemoveEnabled(value: boolean): void;
48
+ /**
49
+ * Whether the in-file search matches letter case exactly. Defaults to off, so a
50
+ * plain query keeps matching either case; only an explicit `'1'` turns it on.
51
+ * @returns whether the search is case-sensitive.
52
+ */
53
+ export declare function searchCaseSensitive(): boolean;
54
+ /** Persist the search case-sensitivity preference. */
55
+ export declare function setSearchCaseSensitive(value: boolean): void;
56
+ /**
57
+ * Whether the in-file search only matches whole words. Defaults to off; only an
58
+ * explicit `'1'` enables it.
59
+ * @returns whether the search matches whole words only.
60
+ */
61
+ export declare function searchWholeWord(): boolean;
62
+ /** Persist the search whole-word preference. */
63
+ export declare function setSearchWholeWord(value: boolean): void;
39
64
  /**
40
65
  * Whether lines wrap (auto-wrap) in the diff for one highlight language.
41
66
  * Defaults to off; only an explicit `'1'` enables it. Stored per language, so
@@ -83,6 +108,22 @@ export declare function setDiffDelColor(value: string): void;
83
108
  export declare function currentDiffAddColor(): string;
84
109
  /** The theme's removed-line base color, for the settings swatch default. */
85
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;
86
127
  /**
87
128
  * Whether the whole-file diff view uses the two-column (side-by-side) layout.
88
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, 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. */
@@ -29,10 +29,11 @@ export interface PendingPanelFace {
29
29
  };
30
30
  /** Read the pending list for the current session into the snapshot. */
31
31
  onRefresh: (sessionId: SessionId | undefined) => void;
32
- /** Keep one operation (remove it from the pending list). */
33
- onKeep: (sessionId: SessionId, id: string) => Promise<void>;
34
- /** Revert one operation (restore its prior content, or remove a created file). */
35
- onRevert: (sessionId: SessionId, id: string) => Promise<void>;
32
+ /** Keep one operation. `keepListed` leaves the resolved entry in the list. */
33
+ onKeep: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
34
+ /** Revert one operation (restore its prior content, or remove a created file).
35
+ * `keepListed` leaves the resolved entry in the list. */
36
+ onRevert: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
36
37
  /** Keep one diff block (accept its change into the tracked baseline). */
37
38
  onBlockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange, removeWhenResolved?: boolean) => Promise<void>;
38
39
  /** Revert one diff block (restore its old lines in the file). */
@@ -50,6 +51,13 @@ export interface PendingPanelFace {
50
51
  onRedo: (sessionId: SessionId) => Promise<string | undefined>;
51
52
  /** Import the workspace's local VCS changes as pending entries (detection included). */
52
53
  onImportVcs: (sessionId: SessionId, includeUntracked: boolean) => Promise<VcsImportValue>;
54
+ /** Replace one entry's diff with the file's current local VCS change, then
55
+ * refresh the list; resolves to what the scan found. */
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>;
53
61
  /** Keep every pending entry of one session in a single host call (bulk). */
54
62
  onKeepAll: (sessionId: SessionId) => Promise<void>;
55
63
  /** Revert every pending entry of one session in a single host call (bulk). */
@@ -8,6 +8,7 @@
8
8
  * @module dsh-diff-approval/client/split-diff
9
9
  */
10
10
  import type { WholeFileDiffRow } from './whole-file-diff.ts';
11
+ import type { SearchOptions } from './search.ts';
11
12
  /** One side of a split pair: text plus its 1-based line number (absent on a pure add). */
12
13
  export interface SplitSide {
13
14
  /** The side's source line content, without the terminating newline. */
@@ -43,12 +44,13 @@ export interface SplitDiff {
43
44
  */
44
45
  export declare function computeSideBySideDiff(rows: readonly WholeFileDiffRow[], alignBySimilarity?: boolean): SplitDiff;
45
46
  /**
46
- * Pair indices whose left or right text contains the query (case-insensitive).
47
- * A pair counts once however many times the query appears, so split search
48
- * highlights the whole pair on both columns and a single "current" pair is
49
- * stepped through — not each individual left/right occurrence.
47
+ * Pair indices whose left or right text matches the query. A pair counts once
48
+ * however many times the query appears, so split search highlights the whole pair
49
+ * on both columns and a single "current" pair is stepped through — not each
50
+ * individual left/right occurrence.
50
51
  * @param pairs - the split pairs.
51
52
  * @param query - the query text; an empty query matches nothing.
53
+ * @param options - case and whole-word narrowing.
52
54
  * @returns matching pair indices, in file order.
53
55
  */
54
- export declare function searchPairs(pairs: readonly SplitPair[], query: string): number[];
56
+ export declare function searchPairs(pairs: readonly SplitPair[], query: string, options?: SearchOptions): number[];
@@ -7,17 +7,17 @@
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, 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. */
14
14
  export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
15
15
  /** Re-read one session's pending list (an absent session empties the view). */
16
16
  refresh: (sessionId: SessionId | undefined) => Promise<void>;
17
- /** Keep one operation. */
18
- keep: (sessionId: SessionId, id: string) => Promise<void>;
19
- /** Revert one operation. */
20
- revert: (sessionId: SessionId, id: string) => Promise<void>;
17
+ /** Keep one operation. `keepListed` leaves the resolved entry in the list. */
18
+ keep: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
19
+ /** Revert one operation. `keepListed` leaves the resolved entry in the list. */
20
+ revert: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
21
21
  /** Keep one diff block, then refresh so the entry's diff reflects the accept. */
22
22
  blockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange, removeWhenResolved?: boolean) => Promise<void>;
23
23
  /** Revert one diff block, then refresh so the entry's diff reflects the undo. */
@@ -28,6 +28,13 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
28
28
  redo: (sessionId: SessionId) => Promise<string | undefined>;
29
29
  /** Import the workspace's local VCS changes as pending entries, then refresh. */
30
30
  importVcs: (sessionId: SessionId, includeUntracked: boolean) => Promise<VcsImportValue>;
31
+ /** Replace one entry's diff with the file's current local VCS change, then
32
+ * refresh; resolves to what the scan found. */
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>;
31
38
  /** Open one file with its default application or reveal it in the folder. */
32
39
  open: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
33
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;
@@ -35,7 +35,7 @@
35
35
  */
36
36
  import { Context } from '@deepseek-ai/cordis';
37
37
  import type { DiffApprovalOpenAction } from './types.ts';
38
- export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBlockTarget, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, PendingEntry, PendingEntryKind, PendingFileDiff, } from './types.ts';
38
+ export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBlockTarget, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalRefreshOutcome, DiffApprovalRefreshValue, PendingEntry, PendingEntryKind, PendingFileDiff, } from './types.ts';
39
39
  export { PendingDiffStore } from './pending.ts';
40
40
  export { PendingPersistence, defaultStorageDir } from './persist.ts';
41
41
  export { defaultOpenPath } from './open.ts';