xtralab 0.15.0 → 0.15.2

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.
Files changed (237) hide show
  1. package/README.md +65 -30
  2. package/lib/about/icons.d.ts +0 -5
  3. package/lib/about/icons.js +0 -5
  4. package/lib/about/index.d.ts +4 -8
  5. package/lib/about/index.js +6 -17
  6. package/lib/agentSessions.d.ts +9 -29
  7. package/lib/agentSessions.js +2 -7
  8. package/lib/askAgent/editorSelection.d.ts +20 -26
  9. package/lib/askAgent/editorSelection.js +11 -26
  10. package/lib/askAgent/icons.d.ts +2 -3
  11. package/lib/askAgent/icons.js +2 -3
  12. package/lib/askAgent/index.d.ts +4 -26
  13. package/lib/askAgent/index.js +54 -158
  14. package/lib/askAgent/popup.d.ts +12 -16
  15. package/lib/askAgent/popup.js +18 -37
  16. package/lib/askAgent/prompt.d.ts +3 -5
  17. package/lib/askAgent/prompt.js +6 -19
  18. package/lib/askAgent/queue.d.ts +11 -21
  19. package/lib/askAgent/queue.js +9 -19
  20. package/lib/askAgent/queuePanel.d.ts +13 -18
  21. package/lib/askAgent/queuePanel.js +15 -37
  22. package/lib/askAgent/targetPicker.d.ts +2 -9
  23. package/lib/askAgent/targetPicker.js +6 -11
  24. package/lib/askAgent/tokens.d.ts +32 -46
  25. package/lib/askAgent/tokens.js +4 -6
  26. package/lib/commandBar/index.d.ts +4 -13
  27. package/lib/commandBar/index.js +21 -39
  28. package/lib/customPanel/index.js +15 -0
  29. package/lib/editorBreadcrumbs/index.d.ts +3 -6
  30. package/lib/editorBreadcrumbs/index.js +10 -6
  31. package/lib/editorBreadcrumbs/widget.d.ts +17 -4
  32. package/lib/editorBreadcrumbs/widget.js +11 -17
  33. package/lib/editorIndent/index.d.ts +4 -12
  34. package/lib/editorIndent/index.js +8 -23
  35. package/lib/fileBrowser/commands.d.ts +29 -19
  36. package/lib/fileBrowser/commands.js +83 -97
  37. package/lib/fileBrowser/contents.d.ts +3 -0
  38. package/lib/fileBrowser/dragAndDrop.d.ts +17 -4
  39. package/lib/fileBrowser/dragAndDrop.js +2 -7
  40. package/lib/fileBrowser/fileBrowser.d.ts +19 -9
  41. package/lib/fileBrowser/fileBrowser.js +70 -222
  42. package/lib/fileBrowser/gitStatus.d.ts +16 -10
  43. package/lib/fileBrowser/gitStatus.js +31 -30
  44. package/lib/fileBrowser/gitignore.d.ts +4 -17
  45. package/lib/fileBrowser/gitignore.js +4 -25
  46. package/lib/fileBrowser/icons.d.ts +7 -17
  47. package/lib/fileBrowser/icons.js +25 -80
  48. package/lib/fileBrowser/index.js +2 -4
  49. package/lib/fileBrowser/widget.d.ts +141 -48
  50. package/lib/fileBrowser/widget.js +102 -4
  51. package/lib/fileTypeIcons/index.js +0 -10
  52. package/lib/git/api.d.ts +5 -13
  53. package/lib/git/api.js +10 -36
  54. package/lib/git/askRequest.d.ts +4 -8
  55. package/lib/git/askRequest.js +6 -20
  56. package/lib/git/commands.d.ts +45 -5
  57. package/lib/git/commands.js +20 -13
  58. package/lib/git/diffModel.js +4 -0
  59. package/lib/git/diffPreferences.d.ts +90 -0
  60. package/lib/git/diffPreferences.js +107 -0
  61. package/lib/git/diffProvider.js +10 -2
  62. package/lib/git/diffSurface.d.ts +64 -63
  63. package/lib/git/diffSurface.js +72 -213
  64. package/lib/git/diffTheme.d.ts +7 -0
  65. package/lib/git/diffTheme.js +23 -0
  66. package/lib/git/diffWidget.d.ts +70 -14
  67. package/lib/git/diffWidget.js +100 -94
  68. package/lib/git/imageDiff.d.ts +14 -6
  69. package/lib/git/imageDiff.js +19 -75
  70. package/lib/git/index.d.ts +0 -3
  71. package/lib/git/index.js +44 -10
  72. package/lib/git/notebookDiff.d.ts +110 -33
  73. package/lib/git/notebookDiff.js +55 -197
  74. package/lib/git/tokens.d.ts +73 -31
  75. package/lib/git/tokens.js +2 -3
  76. package/lib/highlight/index.d.ts +4 -10
  77. package/lib/highlight/index.js +10 -41
  78. package/lib/index.d.ts +0 -9
  79. package/lib/index.js +0 -9
  80. package/lib/launcher/agents.d.ts +55 -51
  81. package/lib/launcher/agents.js +15 -57
  82. package/lib/launcher/availability.d.ts +3 -9
  83. package/lib/launcher/availability.js +3 -9
  84. package/lib/launcher/commands.d.ts +7 -32
  85. package/lib/launcher/commands.js +14 -45
  86. package/lib/launcher/dashboard.d.ts +19 -24
  87. package/lib/launcher/dashboard.js +21 -67
  88. package/lib/launcher/editorRegistry.d.ts +10 -22
  89. package/lib/launcher/editorRegistry.js +14 -18
  90. package/lib/launcher/editors.d.ts +39 -45
  91. package/lib/launcher/editors.js +8 -34
  92. package/lib/launcher/icons.d.ts +0 -12
  93. package/lib/launcher/icons.js +5 -39
  94. package/lib/launcher/index.js +12 -55
  95. package/lib/launcher/invocation.d.ts +4 -10
  96. package/lib/launcher/invocation.js +7 -22
  97. package/lib/launcher/registry.d.ts +9 -7
  98. package/lib/launcher/registry.js +9 -7
  99. package/lib/launcher/tokens.d.ts +10 -22
  100. package/lib/launcher/tokens.js +5 -8
  101. package/lib/menuBar/index.d.ts +3 -10
  102. package/lib/menuBar/index.js +11 -28
  103. package/lib/menus/index.d.ts +3 -9
  104. package/lib/menus/index.js +14 -30
  105. package/lib/omnibox/files.d.ts +2 -4
  106. package/lib/omnibox/files.js +5 -11
  107. package/lib/omnibox/index.d.ts +4 -14
  108. package/lib/omnibox/index.js +9 -27
  109. package/lib/omnibox/model.d.ts +38 -10
  110. package/lib/omnibox/model.js +15 -33
  111. package/lib/omnibox/recents.d.ts +15 -17
  112. package/lib/omnibox/recents.js +12 -17
  113. package/lib/omnibox/tokens.d.ts +5 -10
  114. package/lib/omnibox/tokens.js +2 -6
  115. package/lib/omnibox/widget.d.ts +11 -7
  116. package/lib/omnibox/widget.js +8 -16
  117. package/lib/searchReplace/index.d.ts +4 -20
  118. package/lib/searchReplace/index.js +7 -27
  119. package/lib/showOutput/index.d.ts +3 -12
  120. package/lib/showOutput/index.js +6 -22
  121. package/lib/sidebar/index.d.ts +4 -7
  122. package/lib/sidebar/index.js +5 -12
  123. package/lib/terminalNotifications/index.d.ts +4 -10
  124. package/lib/terminalNotifications/index.js +6 -20
  125. package/lib/terminals/agentTerminals.d.ts +12 -5
  126. package/lib/terminals/agentTerminals.js +25 -44
  127. package/lib/terminals/detection.d.ts +4 -8
  128. package/lib/terminals/detection.js +4 -8
  129. package/lib/terminals/index.d.ts +0 -35
  130. package/lib/terminals/index.js +12 -101
  131. package/lib/terminals/model.d.ts +64 -139
  132. package/lib/terminals/model.js +80 -220
  133. package/lib/terminals/tokens.d.ts +11 -22
  134. package/lib/terminals/tokens.js +2 -2
  135. package/lib/terminals/widget.d.ts +43 -13
  136. package/lib/terminals/widget.js +33 -18
  137. package/lib/topBar/icons.d.ts +3 -8
  138. package/lib/topBar/icons.js +3 -8
  139. package/lib/topBar/index.d.ts +4 -11
  140. package/lib/topBar/index.js +10 -38
  141. package/lib/walkthrough/index.d.ts +4 -9
  142. package/lib/walkthrough/index.js +6 -20
  143. package/lib/walkthrough/panel.d.ts +40 -9
  144. package/lib/walkthrough/panel.js +4 -10
  145. package/package.json +5 -3
  146. package/schema/git-diff-preferences.json +42 -0
  147. package/src/about/icons.ts +0 -5
  148. package/src/about/index.tsx +6 -17
  149. package/src/agentSessions.ts +9 -29
  150. package/src/askAgent/editorSelection.ts +22 -29
  151. package/src/askAgent/icons.ts +2 -3
  152. package/src/askAgent/index.ts +54 -158
  153. package/src/askAgent/popup.tsx +23 -45
  154. package/src/askAgent/prompt.ts +6 -19
  155. package/src/askAgent/queue.ts +13 -24
  156. package/src/askAgent/queuePanel.tsx +21 -46
  157. package/src/askAgent/targetPicker.tsx +5 -15
  158. package/src/askAgent/tokens.ts +32 -46
  159. package/src/commandBar/index.ts +24 -39
  160. package/src/customPanel/index.ts +15 -0
  161. package/src/editorBreadcrumbs/index.ts +10 -6
  162. package/src/editorBreadcrumbs/widget.ts +23 -17
  163. package/src/editorIndent/index.ts +17 -23
  164. package/src/fileBrowser/commands.ts +119 -96
  165. package/src/fileBrowser/contents.ts +3 -0
  166. package/src/fileBrowser/dragAndDrop.ts +17 -7
  167. package/src/fileBrowser/fileBrowser.tsx +86 -230
  168. package/src/fileBrowser/gitStatus.ts +36 -33
  169. package/src/fileBrowser/gitignore.ts +4 -25
  170. package/src/fileBrowser/icons.ts +25 -80
  171. package/src/fileBrowser/index.ts +2 -4
  172. package/src/fileBrowser/widget.tsx +171 -50
  173. package/src/fileTypeIcons/index.ts +0 -10
  174. package/src/git/api.ts +10 -36
  175. package/src/git/askRequest.ts +6 -20
  176. package/src/git/commands.ts +83 -11
  177. package/src/git/diffModel.ts +25 -0
  178. package/src/git/diffPreferences.ts +192 -0
  179. package/src/git/diffProvider.tsx +11 -2
  180. package/src/git/diffSurface.tsx +165 -286
  181. package/src/git/diffTheme.ts +24 -0
  182. package/src/git/diffWidget.tsx +194 -134
  183. package/src/git/imageDiff.tsx +56 -84
  184. package/src/git/index.ts +54 -10
  185. package/src/git/notebookDiff.tsx +214 -240
  186. package/src/git/tokens.ts +73 -31
  187. package/src/highlight/index.ts +10 -41
  188. package/src/index.ts +0 -9
  189. package/src/launcher/agents.ts +64 -95
  190. package/src/launcher/availability.ts +3 -9
  191. package/src/launcher/commands.ts +14 -51
  192. package/src/launcher/dashboard.tsx +49 -88
  193. package/src/launcher/editorRegistry.ts +21 -29
  194. package/src/launcher/editors.ts +39 -58
  195. package/src/launcher/icons.ts +5 -39
  196. package/src/launcher/index.ts +12 -55
  197. package/src/launcher/invocation.ts +7 -22
  198. package/src/launcher/registry.ts +9 -7
  199. package/src/launcher/tokens.ts +10 -22
  200. package/src/menuBar/index.ts +11 -28
  201. package/src/menus/index.ts +14 -30
  202. package/src/omnibox/files.ts +10 -14
  203. package/src/omnibox/index.ts +9 -27
  204. package/src/omnibox/model.ts +50 -38
  205. package/src/omnibox/recents.ts +17 -22
  206. package/src/omnibox/tokens.ts +5 -10
  207. package/src/omnibox/widget.tsx +14 -19
  208. package/src/searchReplace/index.ts +7 -27
  209. package/src/showOutput/index.ts +6 -22
  210. package/src/sidebar/index.ts +20 -12
  211. package/src/terminalNotifications/index.ts +42 -25
  212. package/src/terminals/agentTerminals.ts +25 -49
  213. package/src/terminals/detection.ts +4 -8
  214. package/src/terminals/index.ts +12 -101
  215. package/src/terminals/model.ts +130 -246
  216. package/src/terminals/tokens.ts +11 -22
  217. package/src/terminals/widget.tsx +48 -31
  218. package/src/topBar/icons.ts +3 -8
  219. package/src/topBar/index.ts +21 -44
  220. package/src/walkthrough/index.ts +6 -20
  221. package/src/walkthrough/panel.ts +40 -10
  222. package/style/about.css +2 -7
  223. package/style/askAgent.css +8 -40
  224. package/style/base.css +9 -45
  225. package/style/chrome.css +108 -308
  226. package/style/commandBar.css +4 -20
  227. package/style/customPanel.css +1 -4
  228. package/style/git.css +26 -186
  229. package/style/highlight.css +1 -3
  230. package/style/launcher.css +6 -70
  231. package/style/omnibox.css +2 -14
  232. package/style/showOutput.css +0 -6
  233. package/style/sidebar.css +29 -9
  234. package/style/tabs.css +5 -13
  235. package/style/terminals.css +6 -53
  236. package/style/topBar.css +2 -22
  237. package/style/walkthrough.css +0 -8
@@ -5,6 +5,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
5
5
  import type { TranslationBundle } from '@jupyterlab/translation';
6
6
  import { undoIcon } from '@jupyterlab/ui-components';
7
7
  import { FileDiff } from '@pierre/diffs/react';
8
+ import { IconDiffSplit, IconDiffUnified, IconWordWrap } from '@pierre/icons';
8
9
  import {
9
10
  diffAcceptRejectHunk,
10
11
  parseDiffFromFile,
@@ -20,161 +21,52 @@ import {
20
21
  NotebookDiffView,
21
22
  type INotebookDiffResult
22
23
  } from './notebookDiff';
23
- import { resolveDiffTheme } from './diffTheme';
24
+ import {
25
+ DEFAULT_SPLIT_RATIO,
26
+ MAX_SPLIT_RATIO,
27
+ MIN_SPLIT_RATIO,
28
+ type DiffStyle,
29
+ type DiffStyleControlMode,
30
+ type ImageDiffViewMode,
31
+ type NotebookDiffViewMode
32
+ } from './diffPreferences';
33
+ import { DIFF_SCROLLBAR_CSS, resolveDiffTheme } from './diffTheme';
24
34
 
25
- /**
26
- * The CSS class added to the diff main-area widget. The selectors that
27
- * style the embedded `@pierre/diffs` viewer hang off this class.
28
- */
29
35
  export const DIFF_WIDGET_CSS_CLASS = 'jp-xtralab-DiffWidget';
30
36
 
31
37
  /**
32
- * `localStorage` key for the user's last-chosen split ratio, persisted so
33
- * the preference survives reloads (the library has no built-in way to
34
- * remember it).
35
- */
36
- const SPLIT_RATIO_STORAGE_KEY = 'xtralab:diff-split-ratio';
37
-
38
- /**
39
- * Bounds on the left-pane fraction. Keeping a few percent on either side
40
- * guarantees both panes remain visible — dragging to 0/100 would hide a
41
- * column and is almost never what the user wants.
42
- */
43
- const MIN_SPLIT_RATIO = 0.1;
44
- const MAX_SPLIT_RATIO = 0.9;
45
- const DEFAULT_SPLIT_RATIO = 0.5;
46
-
47
- /**
48
- * CSS injected via the `@pierre/diffs` `unsafeCSS` option. The library
49
- * places it inside the shadow root under `@layer unsafe`, which beats
50
- * the library's own `@layer base` rule that hardcodes
51
- * `grid-template-columns: 1fr 1fr` on the split-mode container.
52
- *
53
- * The override reads from `--xtralab-split-cols`, a custom property we set
54
- * on the host element via the `style` prop. Custom properties inherit
55
- * through the shadow DOM boundary, so adjusting it on the host instantly
56
- * resizes the columns without re-rendering the diff.
38
+ * Injected via the `unsafeCSS` option into the shadow root's `@layer
39
+ * unsafe`, beating the library's base rule that hardcodes `1fr 1fr`. Reads
40
+ * `--xtralab-split-cols` set on the host — custom properties cross the
41
+ * shadow boundary, so resizing needs no re-render.
57
42
  */
58
43
  const SPLIT_RESIZE_CSS = `pre[data-diff-type="split"][data-overflow="scroll"] {
59
44
  grid-template-columns: var(--xtralab-split-cols, 1fr 1fr);
60
45
  }`;
61
46
 
62
- function readStoredSplitRatio(): number {
63
- try {
64
- const raw = window.localStorage.getItem(SPLIT_RATIO_STORAGE_KEY);
65
- if (raw === null) {
66
- return DEFAULT_SPLIT_RATIO;
67
- }
68
- const parsed = Number.parseFloat(raw);
69
- if (
70
- Number.isFinite(parsed) &&
71
- parsed >= MIN_SPLIT_RATIO &&
72
- parsed <= MAX_SPLIT_RATIO
73
- ) {
74
- return parsed;
75
- }
76
- } catch {
77
- // localStorage may throw in privacy mode or sandboxed contexts. Falling
78
- // back to the default is fine — the user can resize again on this run.
79
- }
80
- return DEFAULT_SPLIT_RATIO;
81
- }
82
-
83
- function writeStoredSplitRatio(ratio: number): void {
84
- try {
85
- window.localStorage.setItem(SPLIT_RATIO_STORAGE_KEY, ratio.toString());
86
- } catch {
87
- // See readStoredSplitRatio — best-effort persistence.
88
- }
89
- }
47
+ const DIFF_SURFACE_CSS = `${SPLIT_RESIZE_CSS}\n${DIFF_SCROLLBAR_CSS}`;
90
48
 
91
49
  /**
92
- * Layout for textual/code diffs, forwarded to `@pierre/diffs` as its
93
- * `diffStyle` option. `split` is the side-by-side old|new view; `unified`
94
- * is the single-column inline view (what diffs.com calls "stacked").
95
- */
96
- export type DiffStyle = 'split' | 'unified';
97
-
98
- const DIFF_STYLE_STORAGE_KEY = 'xtralab:diff-style';
99
-
100
- export function readStoredDiffStyle(): DiffStyle {
101
- try {
102
- const raw = window.localStorage.getItem(DIFF_STYLE_STORAGE_KEY);
103
- if (raw === 'split' || raw === 'unified') {
104
- return raw;
105
- }
106
- } catch {
107
- // See readStoredSplitRatio — privacy-mode / sandboxed contexts can throw.
108
- }
109
- return 'split';
110
- }
111
-
112
- export function writeStoredDiffStyle(style: DiffStyle): void {
113
- try {
114
- window.localStorage.setItem(DIFF_STYLE_STORAGE_KEY, style);
115
- } catch {
116
- // Best-effort.
117
- }
118
- }
119
-
120
- /**
121
- * View modes available for `.ipynb` diffs. `notebook` is the cell-by-cell
122
- * rendered view; `json` is the raw nbformat JSON file diff, for inspecting
123
- * exactly which bytes changed.
124
- */
125
- export type NotebookDiffViewMode = 'notebook' | 'json';
126
-
127
- const NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:notebook-diff-view-mode';
128
-
129
- export function readStoredNotebookViewMode(): NotebookDiffViewMode {
130
- try {
131
- const raw = window.localStorage.getItem(
132
- NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY
133
- );
134
- if (raw === 'json' || raw === 'notebook') {
135
- return raw;
136
- }
137
- } catch {
138
- // See readStoredSplitRatio — privacy-mode / sandboxed contexts can
139
- // throw on access. The notebook view is the better default anyway.
140
- }
141
- return 'notebook';
142
- }
143
-
144
- export function writeStoredNotebookViewMode(mode: NotebookDiffViewMode): void {
145
- try {
146
- window.localStorage.setItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY, mode);
147
- } catch {
148
- // Best-effort.
149
- }
150
- }
151
-
152
- /**
153
- * Annotation payload threaded through the diff library back into the
154
- * `renderAnnotation` callback. Carries the index of the hunk a particular
155
- * action button targets.
50
+ * Annotation payload threaded back into `renderAnnotation`; carries the target hunk index.
156
51
  */
157
52
  interface IHunkActionAnnotation {
53
+ /**
54
+ * The index of the hunk the action targets.
55
+ */
158
56
  hunkIndex: number;
159
57
  }
160
58
 
161
59
  /**
162
- * Detect notebook files by extension. We only look at the suffix so the
163
- * caller doesn't need to plumb a content-type field through; mistaking
164
- * another `.ipynb`-named file for a notebook is harmless because
165
- * {@link buildNotebookDiff} validates the JSON shape and we fall back to
166
- * the file diff if parsing fails.
60
+ * Extension-only check; a false positive is harmless because
61
+ * {@link buildNotebookDiff} validates the JSON and the file diff is the fallback.
167
62
  */
168
63
  function isNotebookPath(path: string): boolean {
169
64
  return path.toLowerCase().endsWith('.ipynb');
170
65
  }
171
66
 
172
67
  /**
173
- * Decide whether the active JupyterLab theme is a dark theme. Prefers the
174
- * `IThemeManager` token when available, and falls back to sniffing the
175
- * `data-jp-theme-light` attribute that JupyterLab sets on `<body>` so the
176
- * component still works in stripped-down hosts where the token isn't
177
- * provided (lite, certain notebook configurations, …).
68
+ * Dark-theme check via `IThemeManager`, falling back to the
69
+ * `data-jp-theme-light` body attribute for hosts without the token.
178
70
  */
179
71
  export function isDarkTheme(themeManager: IThemeManager | null): boolean {
180
72
  if (themeManager !== null && themeManager.theme !== null) {
@@ -192,19 +84,27 @@ export function isPierreTheme(themeManager: IThemeManager | null): boolean {
192
84
  }
193
85
 
194
86
  /**
195
- * Optional per-hunk discard wiring. When supplied and `enabled`, each hunk
196
- * in a plain-text/code diff gets an inline "discard" button; clicking it
197
- * reverts that hunk and hands the rebuilt full-file text back to the host
198
- * via {@link IHunkDiscard.save}. The host owns the actual write (it knows
199
- * the on-disk path) and triggers any follow-up refresh in
200
- * {@link IHunkDiscard.onAfterSave}.
87
+ * Per-hunk discard wiring: discarding rebuilds the full file text and the
88
+ * host owns the write via `save`, then refreshes in `onAfterSave`.
201
89
  */
202
90
  interface IHunkDiscard {
91
+ /**
92
+ * Whether hunk discarding is available.
93
+ */
203
94
  enabled: boolean;
95
+ /**
96
+ * Persist the full post-discard file text.
97
+ */
204
98
  save: (fullText: string) => Promise<void>;
99
+ /**
100
+ * Called after a successful save so the host can refresh the diff.
101
+ */
205
102
  onAfterSave: () => void;
206
103
  }
207
104
 
105
+ /**
106
+ * The props for the {@link DiffSurface} component.
107
+ */
208
108
  interface IDiffSurfaceProps {
209
109
  /**
210
110
  * Whether the host is still resolving the file contents.
@@ -227,13 +127,11 @@ interface IDiffSurfaceProps {
227
127
  */
228
128
  newText: string;
229
129
  /**
230
- * New-side path. Drives notebook detection and the `@pierre/diffs` file
231
- * name on the additions side.
130
+ * New-side path; drives notebook detection and the highlighting filename.
232
131
  */
233
132
  newName: string;
234
133
  /**
235
- * Old-side path. Differs from {@link newName} only for renames; defaults
236
- * to {@link newName} when the host does not track a previous path.
134
+ * Old-side path; differs from {@link newName} only for renames.
237
135
  */
238
136
  oldName?: string;
239
137
  /**
@@ -245,35 +143,48 @@ interface IDiffSurfaceProps {
245
143
  */
246
144
  pierreTheme: boolean;
247
145
  /**
248
- * Rendermime registry used by the notebook view to render outputs and
249
- * markdown cells. May be `null` in stripped-down hosts; the notebook
250
- * diff falls back to a textual representation in that case.
146
+ * Used by the notebook view; `null` in stripped-down hosts, which forces a textual fallback.
251
147
  */
252
148
  rendermime: IRenderMimeRegistry | null;
253
149
  /**
254
- * Current rendered-vs-JSON choice for notebook diffs (host-controlled).
150
+ * Rendered-vs-JSON choice for notebook diffs (host-controlled).
255
151
  */
256
152
  notebookViewMode: NotebookDiffViewMode;
257
153
  /**
258
- * Split vs unified layout for the textual/code file diff (host-controlled).
154
+ * Split vs unified layout for the textual file diff (host-controlled).
259
155
  */
260
156
  diffStyle: DiffStyle;
261
157
  /**
262
- * Called whenever the availability of a rendered notebook view changes,
263
- * so the host can show/hide its Notebook/JSON toolbar toggle.
158
+ * Whether long lines wrap instead of scrolling horizontally (host-controlled).
159
+ */
160
+ lineWrap: boolean;
161
+ /**
162
+ * Left pane width in split view, as a fraction of the diff width (host-controlled).
163
+ */
164
+ splitRatio: number;
165
+ /**
166
+ * Called when the user finishes resizing the split panes.
167
+ */
168
+ onSplitRatioChange: (ratio: number) => void;
169
+ /**
170
+ * 2-up, swipe or onion view for image diffs (host-controlled).
171
+ */
172
+ imageViewMode: ImageDiffViewMode;
173
+ /**
174
+ * Called when the user picks another image view mode.
175
+ */
176
+ onImageViewModeChange: (mode: ImageDiffViewMode) => void;
177
+ /**
178
+ * Fires when rendered-notebook availability changes so the host can toggle its Notebook/JSON control.
264
179
  */
265
180
  onNotebookAvailabilityChange?: (available: boolean) => void;
266
181
  /**
267
- * Called whenever the textual/code file diff (the only view the split vs
268
- * unified choice affects) becomes the active view, so the host can
269
- * show/hide its Split/Unified toolbar toggle. False for image, binary,
270
- * rendered-notebook, loading and error states.
182
+ * Fires when the textual file diff becomes (in)active — the only view the split/unified choice affects.
271
183
  */
272
184
  onFileDiffActiveChange?: (active: boolean) => void;
273
185
  /**
274
- * Called after each (re)computation of the diff with the number of hunks
275
- * (or `null` when no textual diff exists). Hosts use this to implement
276
- * post-discard behaviors such as auto-closing an emptied diff.
186
+ * Fires after each diff computation with the hunk count (`null` when no
187
+ * textual diff); hosts use it to auto-close emptied diffs.
277
188
  */
278
189
  onMetadataChange?: (info: { hunkCount: number | null }) => void;
279
190
  /**
@@ -281,11 +192,8 @@ interface IDiffSurfaceProps {
281
192
  */
282
193
  hunkDiscard?: IHunkDiscard;
283
194
  /**
284
- * When provided, the textual/code file diff gets line selection: clicking
285
- * or dragging over the line numbers selects a range, and a gutter "+"
286
- * button appears on the hovered/selected lines. Clicking that button
287
- * invokes this callback with the selected range and the button's viewport
288
- * rectangle (to anchor a popup to), `null` when it cannot be measured.
195
+ * Enables line selection and the gutter "+" button; called with the
196
+ * selected range and the button's viewport rect (`null` when unmeasurable).
289
197
  */
290
198
  onLineAsk?: (range: SelectedLineRange, anchor: DOMRect | null) => void;
291
199
  /**
@@ -311,6 +219,11 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
311
219
  rendermime,
312
220
  notebookViewMode,
313
221
  diffStyle,
222
+ lineWrap,
223
+ splitRatio,
224
+ onSplitRatioChange,
225
+ imageViewMode,
226
+ onImageViewModeChange,
314
227
  onNotebookAvailabilityChange,
315
228
  onFileDiffActiveChange,
316
229
  onMetadataChange,
@@ -321,15 +234,11 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
321
234
 
322
235
  const hasContent = !loading && error === null && !isBinary;
323
236
 
324
- // Raster images take a dedicated `<img>`-based view instead of a
325
- // text/notebook diff; the host passes the two sides as base64 strings in
326
- // `oldText`/`newText` (the git server base64-encodes binary content).
237
+ // For raster images the host passes both sides as base64 in oldText/newText.
327
238
  const imageType = React.useMemo(() => imageDataType(newName), [newName]);
328
239
 
329
- // Pre-compute the line-oriented diff metadata. Owning it here lets us
330
- // pass it to {@link FileDiff} *and* thread the same instance through
331
- // {@link diffAcceptRejectHunk} when the user discards a hunk — so hunk
332
- // indexes line up between what the user clicked and what we mutate.
240
+ // Owning the metadata here keeps hunk indexes aligned between FileDiff
241
+ // and diffAcceptRejectHunk on discard.
333
242
  const metadata = React.useMemo<FileDiffMetadata | null>(() => {
334
243
  if (!hasContent || imageType !== null) {
335
244
  return null;
@@ -342,10 +251,6 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
342
251
  return parseDiffFromFile(oldFile, newFile);
343
252
  }, [hasContent, imageType, oldText, newText, oldName, newName]);
344
253
 
345
- // Cell-by-cell notebook diff for `.ipynb` files. When non-null the
346
- // renderer uses a notebook-aware view instead of the line-oriented file
347
- // diff — kept distinct from {@link metadata} so a notebook whose JSON we
348
- // cannot parse can fall back to the file diff path.
349
254
  const notebookDiff = React.useMemo<INotebookDiffResult | null>(() => {
350
255
  if (!hasContent || imageType !== null || !isNotebookPath(newName)) {
351
256
  return null;
@@ -353,43 +258,33 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
353
258
  return buildNotebookDiff({ oldText, newText });
354
259
  }, [hasContent, imageType, oldText, newText, newName]);
355
260
 
356
- // Tell the host whether a rendered notebook view is currently available
357
- // so it can show/hide its Notebook/JSON toggle.
358
261
  React.useEffect(() => {
359
262
  onNotebookAvailabilityChange?.(notebookDiff !== null);
360
263
  }, [onNotebookAvailabilityChange, notebookDiff]);
361
264
 
362
- // Report the diff shape so hosts can react (e.g. auto-close on empty).
363
265
  React.useEffect(() => {
364
266
  onMetadataChange?.({
365
267
  hunkCount: metadata !== null ? metadata.hunks.length : null
366
268
  });
367
269
  }, [onMetadataChange, metadata]);
368
270
 
369
- // Decide which view to render. The notebook view requires a successful
370
- // nbformat parse; if that failed, we only show the raw JSON file diff.
371
- // Computed before the early returns below so the file-diff-active effect
372
- // can depend on it without violating the rules of hooks.
271
+ // Computed before the early returns so the hooks below can depend on
272
+ // them without breaking the rules of hooks.
373
273
  const hasNotebookView = notebookDiff !== null;
374
274
  const showNotebookView = hasNotebookView && notebookViewMode === 'notebook';
375
275
  const showFileDiff = !showNotebookView && metadata !== null;
376
276
 
377
- // The split vs unified choice only affects the textual/code file diff, so
378
- // tell the host whether that view is active to drive its Split/Unified
379
- // toolbar toggle.
380
277
  React.useEffect(() => {
381
278
  onFileDiffActiveChange?.(showFileDiff);
382
279
  }, [onFileDiffActiveChange, showFileDiff]);
383
280
 
384
- // Split ratio for the diff columns: fraction of width given to the
385
- // deletions (left) pane. Persisted across sessions so the user only
386
- // dials in their layout once.
387
- const [leftRatio, setLeftRatio] = React.useState<number>(() =>
388
- readStoredSplitRatio()
389
- );
281
+ // Local while dragging; the host hears the ratio when a drag ends.
282
+ const [leftRatio, setLeftRatio] = React.useState<number>(splitRatio);
283
+ React.useEffect(() => {
284
+ setLeftRatio(splitRatio);
285
+ }, [splitRatio]);
390
286
  const wrapperRef = React.useRef<HTMLDivElement | null>(null);
391
- // The drag handler reads the live ratio from a ref so the listeners we
392
- // attach on pointer-down don't capture a stale value across renders.
287
+ // Drag listeners read the live ratio from a ref to avoid stale captures.
393
288
  const leftRatioRef = React.useRef(leftRatio);
394
289
  React.useEffect(() => {
395
290
  leftRatioRef.current = leftRatio;
@@ -405,8 +300,6 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
405
300
  }
406
301
  return metadata.hunks.map((hunk, hunkIndex) => ({
407
302
  side: 'additions',
408
- // The first line of each hunk in the new file. We anchor the button
409
- // there so it sits at the top of the hunk's body.
410
303
  lineNumber: hunk.additionStart,
411
304
  metadata: { hunkIndex }
412
305
  }));
@@ -418,12 +311,8 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
418
311
  return;
419
312
  }
420
313
  const updated = diffAcceptRejectHunk(metadata, hunkIndex, 'reject');
421
- // `additionLines` after a 'reject' contains the full new file with
422
- // the hunk reverted back to the old content. The library splits
423
- // file contents with `/(?<=\n)/` (a lookbehind that keeps the
424
- // trailing `\n` on each line), so the entries already carry their
425
- // own line endings — joining with `''` rebuilds the original text
426
- // verbatim, while joining with `'\n'` would double every newline.
314
+ // additionLines holds the full new file with the hunk reverted; lines keep
315
+ // their trailing `\n` (lookbehind split), so join('') rebuilds it verbatim.
427
316
  const text = updated.additionLines.join('');
428
317
  try {
429
318
  await hunkDiscard.save(text);
@@ -469,9 +358,7 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
469
358
  if (onLineAsk === undefined) {
470
359
  return;
471
360
  }
472
- // Anchor to the gutter "+" button that was just clicked. It lives in
473
- // the `@pierre/diffs` shadow root (which is open), parked on the
474
- // hovered/selected line's number element.
361
+ // Anchor to the just-clicked gutter "+" button inside the library's open shadow root.
475
362
  const slot = wrapperRef.current
476
363
  ?.querySelector('diffs-container')
477
364
  ?.shadowRoot?.querySelector('[data-gutter-utility-slot]');
@@ -485,8 +372,6 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
485
372
 
486
373
  const handleResizerPointerDown = React.useCallback(
487
374
  (event: React.PointerEvent<HTMLDivElement>) => {
488
- // Only react to primary-button drags. Right-click / middle-click on
489
- // the handle should pass through to the browser default.
490
375
  if (event.button !== 0) {
491
376
  return;
492
377
  }
@@ -519,21 +404,19 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
519
404
  handle.removeEventListener('pointermove', onPointerMove);
520
405
  handle.removeEventListener('pointerup', onPointerEnd);
521
406
  handle.removeEventListener('pointercancel', onPointerEnd);
522
- // Only persist on drag-end so a quick drag isn't followed by a
523
- // burst of writes for every intermediate position.
524
- writeStoredSplitRatio(leftRatioRef.current);
407
+ onSplitRatioChange(leftRatioRef.current);
525
408
  };
526
409
  handle.addEventListener('pointermove', onPointerMove);
527
410
  handle.addEventListener('pointerup', onPointerEnd);
528
411
  handle.addEventListener('pointercancel', onPointerEnd);
529
412
  },
530
- []
413
+ [onSplitRatioChange]
531
414
  );
532
415
 
533
416
  const handleResizerDoubleClick = React.useCallback(() => {
534
417
  setLeftRatio(DEFAULT_SPLIT_RATIO);
535
- writeStoredSplitRatio(DEFAULT_SPLIT_RATIO);
536
- }, []);
418
+ onSplitRatioChange(DEFAULT_SPLIT_RATIO);
419
+ }, [onSplitRatioChange]);
537
420
 
538
421
  const handleResizerKeyDown = React.useCallback(
539
422
  (event: React.KeyboardEvent<HTMLDivElement>) => {
@@ -562,9 +445,9 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
562
445
  Math.min(MAX_SPLIT_RATIO, next)
563
446
  );
564
447
  setLeftRatio(clamped);
565
- writeStoredSplitRatio(clamped);
448
+ onSplitRatioChange(clamped);
566
449
  },
567
- []
450
+ [onSplitRatioChange]
568
451
  );
569
452
 
570
453
  if (loading) {
@@ -589,15 +472,14 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
589
472
  );
590
473
  }
591
474
  if (imageType !== null) {
592
- // `oldText`/`newText` carry the two revisions as base64 here. The
593
- // image view owns its own layout and mode selector, so it does not
594
- // use the split resizer or the notebook/JSON toggle.
595
475
  return (
596
476
  <div className="jp-xtralab-DiffWidget-content">
597
477
  <ImageDiffView
598
478
  reference={oldText}
599
479
  challenger={newText}
600
480
  fileType={imageType}
481
+ mode={imageViewMode}
482
+ onModeChange={onImageViewModeChange}
601
483
  trans={trans}
602
484
  />
603
485
  </div>
@@ -611,11 +493,10 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
611
493
  );
612
494
  }
613
495
 
614
- // Custom property set on the diffs-container host. The library's
615
- // shadow-root rule consumes it via `var(--xtralab-split-cols, …)` (see
616
- // SPLIT_RESIZE_CSS) and recomputes the column tracks instantly. Only
617
- // relevant in the file-diff path; the notebook view doesn't use it.
618
496
  const leftPercent = leftRatio * 100;
497
+ // The wrapped split grid sizes its gutter tracks to their content, so a
498
+ // percentage split cannot line up with the handle.
499
+ const showResizer = showFileDiff && diffStyle === 'split' && !lineWrap;
619
500
  const hostStyle = {
620
501
  '--xtralab-split-cols': `${leftPercent}% ${100 - leftPercent}%`
621
502
  } as React.CSSProperties;
@@ -638,30 +519,17 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
638
519
  lineAnnotations={lineAnnotations}
639
520
  renderAnnotation={renderAnnotation}
640
521
  style={hostStyle}
641
- // Disable the worker pool: JupyterLab's webpack federation
642
- // pipeline doesn't ship the library's
643
- // `@pierre/diffs/worker/worker.js` file at a URL the worker
644
- // bootstrap can resolve from, so leaving the pool enabled
645
- // crashes on instantiation. Running on the main thread is
646
- // fine for the small files git diffs typically operate on.
522
+ // JupyterLab's federation pipeline can't serve the worker bundle
523
+ // at a resolvable URL, so the pool crashes; run on the main thread.
647
524
  disableWorkerPool={true}
648
525
  options={{
649
526
  diffStyle,
650
- // Drop the file header — the tab title already shows the
651
- // file name and the panel header carries the change
652
- // context.
527
+ overflow: lineWrap ? 'wrap' : 'scroll',
653
528
  disableFileHeader: true,
654
529
  theme: resolveDiffTheme(dark, pierreTheme),
655
530
  themeType: dark ? 'dark' : 'light',
656
- // Inject the column-resize override into the shadow root
657
- // via the library's `@layer unsafe` channel. Keeping the
658
- // string constant lets the library short-circuit the
659
- // re-render path it uses when this option actually
660
- // changes.
661
- unsafeCSS: SPLIT_RESIZE_CSS,
662
- // Line selection + the gutter "+" button feed the
663
- // ask-agent popup; only wired up when a handler exists so
664
- // a plain diff keeps its passive gutter.
531
+ // Constant string lets the library skip its unsafeCSS re-render path.
532
+ unsafeCSS: DIFF_SURFACE_CSS,
665
533
  ...(onLineAsk !== undefined
666
534
  ? {
667
535
  enableLineSelection: true,
@@ -673,7 +541,7 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
673
541
  />
674
542
  ) : null}
675
543
  </div>
676
- {showFileDiff && diffStyle === 'split' ? (
544
+ {showResizer ? (
677
545
  <div
678
546
  className="jp-xtralab-DiffWidget-resizer"
679
547
  style={{ left: `${leftPercent}%` }}
@@ -698,10 +566,8 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
698
566
  }
699
567
 
700
568
  /**
701
- * Segmented Notebook/JSON selector. Hosts mount this into whatever toolbar
702
- * they own (the launcher's `MainAreaWidget` toolbar, or the
703
- * `jupyterlab-git`-provided diff toolbar) and drive its value/visibility
704
- * from the same state they pass to {@link DiffSurface}.
569
+ * Segmented Notebook/JSON selector. Hosts mount it into their own toolbar
570
+ * and drive value/visibility from the state they pass to {@link DiffSurface}.
705
571
  */
706
572
  export function NotebookViewModeControl(props: {
707
573
  mode: NotebookDiffViewMode;
@@ -744,59 +610,41 @@ export function NotebookViewModeControl(props: {
744
610
  }
745
611
 
746
612
  /**
747
- * Split / unified glyphs, inlined from the `@pierre/diffs` icon sprite
748
- * (`diffs-icon-diff-split` / `diffs-icon-diff-unified`) so the toggle looks
749
- * like the one on the library's own docs site without depending on the
750
- * sprite sheet being injected into the document. `currentColor` lets the
751
- * segmented-button styling drive the fill (including the active state).
752
- */
753
- function DiffSplitIcon(): React.ReactElement {
754
- return (
755
- <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
756
- <path d="M14 0H8.5v16H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2m-1.5 6.5v1h1a.5.5 0 0 1 0 1h-1v1a.5.5 0 0 1-1 0v-1h-1a.5.5 0 0 1 0-1h1v-1a.5.5 0 0 1 1 0" />
757
- <path
758
- d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h5.5V0zm.5 7.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1"
759
- opacity=".3"
760
- />
761
- </svg>
762
- );
763
- }
764
-
765
- function DiffUnifiedIcon(): React.ReactElement {
766
- return (
767
- <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
768
- <path
769
- fillRule="evenodd"
770
- d="M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V8.5h16zm-8-4a.5.5 0 0 0-.5.5v1h-1a.5.5 0 0 0 0 1h1v1a.5.5 0 0 0 1 0v-1h1a.5.5 0 0 0 0-1h-1v-1A.5.5 0 0 0 8 10"
771
- clipRule="evenodd"
772
- />
773
- <path
774
- fillRule="evenodd"
775
- d="M14 0a2 2 0 0 1 2 2v5.5H0V2a2 2 0 0 1 2-2zM6.5 3.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z"
776
- clipRule="evenodd"
777
- opacity=".4"
778
- />
779
- </svg>
780
- );
781
- }
782
-
783
- /**
784
- * Segmented Split/Unified selector. Mirrors {@link NotebookViewModeControl}:
785
- * hosts mount it into their own toolbar and drive its value/visibility from
786
- * the same state they pass to {@link DiffSurface}. Only meaningful while the
787
- * textual/code file diff is the active view, so `available` mirrors the
788
- * surface's `onFileDiffActiveChange`.
613
+ * Split/Unified selector: two segmented buttons, or with `control: 'toggle'`
614
+ * one button that shows the layout it switches to.
615
+ * `available` mirrors the surface's `onFileDiffActiveChange`.
789
616
  */
790
617
  export function DiffStyleControl(props: {
791
618
  diffStyle: DiffStyle;
619
+ control: DiffStyleControlMode;
792
620
  available: boolean;
793
621
  onChange: (style: DiffStyle) => void;
794
622
  trans: TranslationBundle;
795
623
  }): React.ReactElement {
796
- const { diffStyle, available, onChange, trans } = props;
624
+ const { diffStyle, control, available, onChange, trans } = props;
797
625
  if (!available) {
798
626
  return <></>;
799
627
  }
628
+ if (control === 'toggle') {
629
+ const next: DiffStyle = diffStyle === 'split' ? 'unified' : 'split';
630
+ const label =
631
+ next === 'split'
632
+ ? trans.__('Switch to split view')
633
+ : trans.__('Switch to unified view');
634
+ return (
635
+ <div className="jp-xtralab-DiffWidget-segmented">
636
+ <button
637
+ type="button"
638
+ className="jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon"
639
+ title={label}
640
+ aria-label={label}
641
+ onClick={() => onChange(next)}
642
+ >
643
+ {next === 'split' ? <IconDiffSplit /> : <IconDiffUnified />}
644
+ </button>
645
+ </div>
646
+ );
647
+ }
800
648
  return (
801
649
  <div
802
650
  className="jp-xtralab-DiffWidget-segmented"
@@ -813,7 +661,7 @@ export function DiffStyleControl(props: {
813
661
  aria-label={trans.__('Split view')}
814
662
  onClick={() => onChange('split')}
815
663
  >
816
- <DiffSplitIcon />
664
+ <IconDiffSplit />
817
665
  </button>
818
666
  <button
819
667
  type="button"
@@ -825,7 +673,38 @@ export function DiffStyleControl(props: {
825
673
  aria-label={trans.__('Unified view')}
826
674
  onClick={() => onChange('unified')}
827
675
  >
828
- <DiffUnifiedIcon />
676
+ <IconDiffUnified />
677
+ </button>
678
+ </div>
679
+ );
680
+ }
681
+
682
+ /**
683
+ * Line-wrap toggle styled as a one-button segmented control.
684
+ * `available` mirrors the surface's `onFileDiffActiveChange`.
685
+ */
686
+ export function LineWrapControl(props: {
687
+ lineWrap: boolean;
688
+ available: boolean;
689
+ onChange: (wrap: boolean) => void;
690
+ trans: TranslationBundle;
691
+ }): React.ReactElement {
692
+ const { lineWrap, available, onChange, trans } = props;
693
+ if (!available) {
694
+ return <></>;
695
+ }
696
+ return (
697
+ <div className="jp-xtralab-DiffWidget-segmented">
698
+ <button
699
+ type="button"
700
+ aria-pressed={lineWrap}
701
+ data-active={lineWrap}
702
+ className="jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon"
703
+ title={trans.__('Wrap long lines')}
704
+ aria-label={trans.__('Wrap lines')}
705
+ onClick={() => onChange(!lineWrap)}
706
+ >
707
+ <IconWordWrap />
829
708
  </button>
830
709
  </div>
831
710
  );