xtralab 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  An opinionated JupyterLab meta-package.
4
4
 
5
- Bundles a curated set of extensions, a path-first file browser, a VS Code-style
6
- git changes panel, an agent-focused launcher, and a quieter default workspace.
5
+ Bundles a curated set of extensions, a path-first file browser, the
6
+ `jupyterlab-git` panel with its text, notebook and image diffs rendered by
7
+ xtralab, an agent-focused launcher, and a quieter default workspace.
7
8
 
8
9
  ![xtralab screenshot](./screenshot.png)
9
10
 
@@ -38,7 +39,8 @@ instructions.
38
39
 
39
40
  - [`ajlab`](https://github.com/jtpio/ajlab) — agent-ready JupyterLab base
40
41
  - JupyterLab 4.6+
41
- - `jupyterlab-git` — backs the bundled changes panel
42
+ - `jupyterlab-git` — provides the git panel; xtralab swaps its text,
43
+ notebook and image diff rendering for its own
42
44
  - `jupyterlab-lsp` + `ty` — Python LSP via Astral's `ty` (bundled); also
43
45
  detects `typescript-language-server` on `PATH` for JS/TS
44
46
  - `jupyterlab-quickopen`
@@ -48,7 +50,10 @@ instructions.
48
50
  The bundled labextension adds:
49
51
 
50
52
  - A path-first file browser in the left sidebar.
51
- - A "Source Control" panel powered by `jupyterlab-git` and `@pierre/diffs`.
53
+ - xtralab-rendered diffs registered as `jupyterlab-git`'s diff providers
54
+ (its own notebook/plain-text/image diff plugins are disabled) so the
55
+ upstream git panel shows xtralab's diffs: `@pierre/diffs` for text and
56
+ notebooks, and an `<img>`-based 2-up/swipe/onion-skin view for images.
52
57
  - An agent launcher with a prompt textarea, a row of agent buttons (Claude,
53
58
  Codex, Gemini, Copilot, Goose, OpenCode, Kiro, Mistral Vibe), and a
54
59
  collapsible list of changed files. Buttons are filtered to agents installed
@@ -1,17 +1,16 @@
1
1
  import { JupyterFrontEnd } from '@jupyterlab/application';
2
- import { ICommandPalette, IThemeManager } from '@jupyterlab/apputils';
2
+ import { IThemeManager } from '@jupyterlab/apputils';
3
3
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
4
  import { Contents } from '@jupyterlab/services';
5
5
  import { createGitDiffWidget, diffWidgetId } from './diffWidget';
6
- import { GitPanel } from './panel';
7
6
  import { IFileChange } from './tokens';
8
7
  /**
9
- * Command IDs exposed by the git plugin. Both are registered in
10
- * {@link registerGitCommands} so other extensions, the command palette and
11
- * keybindings can drive the panel without going through the React internals.
8
+ * Command IDs exposed by the launcher's git diff path. The launcher
9
+ * dashboard's "Changes" section drives {@link CommandIDs.openDiff} to open
10
+ * its own diff tab, independent of `jupyterlab-git` (whose panel reaches
11
+ * the same rendering via the providers in `diffProvider.tsx`).
12
12
  */
13
13
  export declare namespace CommandIDs {
14
- const refresh = "xtralab:git:refresh";
15
14
  const openDiff = "xtralab:git:open-diff";
16
15
  }
17
16
  /**
@@ -29,9 +28,7 @@ export declare namespace CommandArguments {
29
28
  }
30
29
  export interface IRegisterGitCommandsOptions {
31
30
  app: JupyterFrontEnd;
32
- panel: GitPanel;
33
31
  themeManager: IThemeManager | null;
34
- commandPalette: ICommandPalette | null;
35
32
  /**
36
33
  * Contents manager used by the diff widget to write hunk-discard
37
34
  * results back to the working tree.
@@ -46,7 +43,10 @@ export interface IRegisterGitCommandsOptions {
46
43
  rendermime: IRenderMimeRegistry | null;
47
44
  /**
48
45
  * Called whenever the diff widget mutates the working tree (e.g. after
49
- * a hunk discard) so the plugin can refresh the changes panel.
46
+ * a hunk discard). The launcher dashboard polls git status on its own
47
+ * cadence and `jupyterlab-git`'s panel auto-refreshes on the contents
48
+ * `fileChanged` signal the save emits, so this is a hook for any extra
49
+ * bookkeeping rather than a required refresh.
50
50
  */
51
51
  onChanged: () => void;
52
52
  /**
@@ -63,9 +63,8 @@ export interface IRegisterGitCommandsOptions {
63
63
  onPinned(widget: ReturnType<typeof createGitDiffWidget>): void;
64
64
  }
65
65
  /**
66
- * Register the git commands on the application command registry. Returns
67
- * a disposer that clears the command palette items the registration created
68
- * (the commands themselves stay registered for the lifetime of the plugin).
66
+ * Register the launcher's git diff command on the application command
67
+ * registry.
69
68
  */
70
- export declare function registerGitCommands(options: IRegisterGitCommandsOptions): () => void;
69
+ export declare function registerGitCommands(options: IRegisterGitCommandsOptions): void;
71
70
  export { diffWidgetId };
@@ -1,31 +1,21 @@
1
- import { refreshIcon } from '@jupyterlab/ui-components';
2
1
  import { createGitDiffWidget, diffWidgetId, updateGitDiffWidget } from './diffWidget';
3
2
  /**
4
- * Command IDs exposed by the git plugin. Both are registered in
5
- * {@link registerGitCommands} so other extensions, the command palette and
6
- * keybindings can drive the panel without going through the React internals.
3
+ * Command IDs exposed by the launcher's git diff path. The launcher
4
+ * dashboard's "Changes" section drives {@link CommandIDs.openDiff} to open
5
+ * its own diff tab, independent of `jupyterlab-git` (whose panel reaches
6
+ * the same rendering via the providers in `diffProvider.tsx`).
7
7
  */
8
8
  export var CommandIDs;
9
9
  (function (CommandIDs) {
10
- CommandIDs.refresh = 'xtralab:git:refresh';
11
10
  CommandIDs.openDiff = 'xtralab:git:open-diff';
12
11
  })(CommandIDs || (CommandIDs = {}));
13
12
  /**
14
- * Register the git commands on the application command registry. Returns
15
- * a disposer that clears the command palette items the registration created
16
- * (the commands themselves stay registered for the lifetime of the plugin).
13
+ * Register the launcher's git diff command on the application command
14
+ * registry.
17
15
  */
18
16
  export function registerGitCommands(options) {
19
- const { app, panel, themeManager, commandPalette, contentsManager, rendermime, onChanged, trackDiff, findDiff, onPinned } = options;
17
+ const { app, themeManager, contentsManager, rendermime, onChanged, trackDiff, findDiff, onPinned } = options;
20
18
  const { commands } = app;
21
- commands.addCommand(CommandIDs.refresh, {
22
- label: 'Refresh Git Changes',
23
- caption: 'Refresh the git changes panel',
24
- icon: refreshIcon,
25
- execute: () => {
26
- panel.refresh();
27
- }
28
- });
29
19
  commands.addCommand(CommandIDs.openDiff, {
30
20
  label: 'Open Git Diff',
31
21
  caption: 'Open a side-by-side diff for a changed file',
@@ -58,16 +48,5 @@ export function registerGitCommands(options) {
58
48
  app.shell.activateById(widget.id);
59
49
  }
60
50
  });
61
- const paletteItems = commandPalette
62
- ? [
63
- commandPalette.addItem({
64
- command: CommandIDs.refresh,
65
- category: 'Git'
66
- })
67
- ]
68
- : [];
69
- return () => {
70
- paletteItems.forEach(item => item.dispose());
71
- };
72
51
  }
73
52
  export { diffWidgetId };
@@ -0,0 +1,8 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ /**
3
+ * Registers xtralab's diff rendering as jupyterlab-git's diff providers:
4
+ * `@pierre/diffs` for text and notebooks, the bundled `<img>`-based view
5
+ * for raster images.
6
+ */
7
+ declare const diffProviderPlugin: JupyterFrontEndPlugin<void>;
8
+ export default diffProviderPlugin;
@@ -0,0 +1,314 @@
1
+ import * as React from 'react';
2
+ import { IThemeManager } from '@jupyterlab/apputils';
3
+ import { PathExt } from '@jupyterlab/coreutils';
4
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
5
+ import { ReactWidget } from '@jupyterlab/ui-components';
6
+ import { Git, IGitExtension } from '@jupyterlab/git';
7
+ import { PromiseDelegate } from '@lumino/coreutils';
8
+ import { Signal } from '@lumino/signaling';
9
+ import { DIFF_WIDGET_CSS_CLASS, DiffSurface, NotebookViewModeControl, isDarkTheme, readStoredNotebookViewMode, writeStoredNotebookViewMode } from './diffSurface';
10
+ import { IMAGE_DIFF_EXTENSIONS, imageDataType } from './imageDiff';
11
+ /**
12
+ * A `Git.Diff.IDiffWidget` that renders through {@link DiffSurface}.
13
+ *
14
+ * `jupyterlab-git` constructs this via the registered factory, hosts it in
15
+ * its own `PreviewMainAreaWidget`, and reads `model` / calls `refresh()` /
16
+ * `getResolvedFile()` on it. It owns the notebook view-mode state (mounted
17
+ * into the diff toolbar `jupyterlab-git` hands the factory) and a reload
18
+ * nonce so `refresh()` re-pulls the model's content.
19
+ */
20
+ class XtralabDiffWidget extends ReactWidget {
21
+ constructor(model, context) {
22
+ super();
23
+ this._reloadNonce = 0;
24
+ this._pendingRefresh = null;
25
+ this._notebookViewModeChanged = new Signal(this);
26
+ this._hasNotebookView = false;
27
+ this._hasNotebookViewChanged = new Signal(this);
28
+ this._model = model;
29
+ this._context = context;
30
+ this._notebookViewMode = readStoredNotebookViewMode();
31
+ // `jp-git-diff-root` borrows jupyterlab-git's host sizing (height:100%
32
+ // flex column inside its PreviewMainAreaWidget); `jp-xtralab-DiffWidget`
33
+ // brings our diff theming + the `.jp-xtralab-DiffWidget-*` styles.
34
+ this.addClass('jp-git-diff-root');
35
+ this.addClass(DIFF_WIDGET_CSS_CLASS);
36
+ }
37
+ /** The diff model, as required by `Git.Diff.IDiffWidget`. */
38
+ get model() {
39
+ return this._model;
40
+ }
41
+ /**
42
+ * We render a two-way (reference vs. challenger) view and do not provide
43
+ * a merge editor, so there is nothing to leave unresolved — report
44
+ * resolved so jupyterlab-git's merge-conflict flow is not blocked.
45
+ */
46
+ get isFileResolved() {
47
+ return true;
48
+ }
49
+ /**
50
+ * Used by jupyterlab-git's "Mark as resolved" button. For merge
51
+ * conflicts, jupyterlab-git passes the working-tree result as `base`;
52
+ * keep that content rather than overwriting it with either compared side.
53
+ */
54
+ async getResolvedFile() {
55
+ const source = this._model.hasConflict === true && this._model.base !== undefined
56
+ ? this._model.base
57
+ : this._model.challenger;
58
+ const content = await source.content();
59
+ return {
60
+ type: 'file',
61
+ format: imageDataType(this._model.filename) !== null ? 'base64' : 'text',
62
+ content
63
+ };
64
+ }
65
+ /**
66
+ * Re-pull both sides of the model and re-render. jupyterlab-git wires a
67
+ * "Refresh" toolbar button to this whenever `model.changed` fires (e.g.
68
+ * the compared `HEAD` moved).
69
+ */
70
+ async refresh() {
71
+ const done = new PromiseDelegate();
72
+ this._pendingRefresh = done;
73
+ this._reloadNonce += 1;
74
+ this.update();
75
+ await done.promise;
76
+ }
77
+ /** The notebook rendered-vs-JSON choice; mirrored into the toolbar. */
78
+ get notebookViewMode() {
79
+ return this._notebookViewMode;
80
+ }
81
+ setNotebookViewMode(mode) {
82
+ if (mode === this._notebookViewMode) {
83
+ return;
84
+ }
85
+ this._notebookViewMode = mode;
86
+ writeStoredNotebookViewMode(mode);
87
+ this._notebookViewModeChanged.emit(mode);
88
+ }
89
+ get notebookViewModeChanged() {
90
+ return this._notebookViewModeChanged;
91
+ }
92
+ get hasNotebookView() {
93
+ return this._hasNotebookView;
94
+ }
95
+ setHasNotebookView(value) {
96
+ if (value === this._hasNotebookView) {
97
+ return;
98
+ }
99
+ this._hasNotebookView = value;
100
+ this._hasNotebookViewChanged.emit(value);
101
+ }
102
+ get hasNotebookViewChanged() {
103
+ return this._hasNotebookViewChanged;
104
+ }
105
+ /** Resolve the in-flight {@link refresh} promise, if any. */
106
+ settleRefresh() {
107
+ const pending = this._pendingRefresh;
108
+ this._pendingRefresh = null;
109
+ pending === null || pending === void 0 ? void 0 : pending.resolve();
110
+ }
111
+ get reloadNonce() {
112
+ return this._reloadNonce;
113
+ }
114
+ get context() {
115
+ return this._context;
116
+ }
117
+ render() {
118
+ return React.createElement(ModelDiffView, { widget: this });
119
+ }
120
+ }
121
+ function ModelDiffView(props) {
122
+ const { widget } = props;
123
+ const model = widget.model;
124
+ const { contentsManager, rendermime, themeManager } = widget.context;
125
+ const nonce = widget.reloadNonce;
126
+ const [state, setState] = React.useState({
127
+ loading: true,
128
+ oldText: '',
129
+ newText: '',
130
+ error: null
131
+ });
132
+ // Mirror the widget's notebook view mode so toolbar-driven changes
133
+ // re-render the diff. The widget owns the canonical value (so the
134
+ // toolbar can read/write it without going through React).
135
+ const [notebookViewMode, setNotebookViewMode] = React.useState(() => widget.notebookViewMode);
136
+ React.useEffect(() => {
137
+ const handler = (_sender, mode) => {
138
+ setNotebookViewMode(mode);
139
+ };
140
+ widget.notebookViewModeChanged.connect(handler);
141
+ setNotebookViewMode(widget.notebookViewMode);
142
+ return () => {
143
+ widget.notebookViewModeChanged.disconnect(handler);
144
+ };
145
+ }, [widget]);
146
+ const handleNotebookAvailabilityChange = React.useCallback((available) => {
147
+ widget.setHasNotebookView(available);
148
+ }, [widget]);
149
+ const [dark, setDark] = React.useState(() => isDarkTheme(themeManager));
150
+ React.useEffect(() => {
151
+ if (themeManager !== null) {
152
+ const handler = () => setDark(isDarkTheme(themeManager));
153
+ themeManager.themeChanged.connect(handler);
154
+ return () => {
155
+ themeManager.themeChanged.disconnect(handler);
156
+ };
157
+ }
158
+ const observer = new MutationObserver(() => setDark(isDarkTheme(themeManager)));
159
+ observer.observe(document.body, {
160
+ attributes: true,
161
+ attributeFilter: ['data-jp-theme-light']
162
+ });
163
+ return () => {
164
+ observer.disconnect();
165
+ };
166
+ }, [themeManager]);
167
+ // Fetch both sides whenever the model or the reload nonce changes. The
168
+ // model's `content()` getters target the right git refs internally
169
+ // (working tree, index, a commit, …) so we stay content-source-agnostic.
170
+ React.useEffect(() => {
171
+ let cancelled = false;
172
+ setState({ loading: true, oldText: '', newText: '', error: null });
173
+ void (async () => {
174
+ try {
175
+ const [oldText, newText] = await Promise.all([
176
+ model.reference.content(),
177
+ model.challenger.content()
178
+ ]);
179
+ if (cancelled) {
180
+ return;
181
+ }
182
+ setState({
183
+ loading: false,
184
+ oldText: oldText !== null && oldText !== void 0 ? oldText : '',
185
+ newText: newText !== null && newText !== void 0 ? newText : '',
186
+ error: null
187
+ });
188
+ }
189
+ catch (err) {
190
+ if (cancelled) {
191
+ return;
192
+ }
193
+ setState({
194
+ loading: false,
195
+ oldText: '',
196
+ newText: '',
197
+ error: err instanceof Error ? err.message : String(err)
198
+ });
199
+ }
200
+ finally {
201
+ if (!cancelled) {
202
+ // Let an in-flight `widget.refresh()` resolve once the reload
203
+ // has settled (jupyterlab-git only awaits it to hide its
204
+ // refresh button).
205
+ widget.settleRefresh();
206
+ }
207
+ }
208
+ })();
209
+ return () => {
210
+ cancelled = true;
211
+ };
212
+ }, [widget, model, nonce]);
213
+ // Per-hunk discard is only meaningful when the challenger is the working
214
+ // tree (an unstaged change) and we are not in a three-way merge view.
215
+ const canDiscardHunk = model.challenger.source === Git.Diff.SpecialRef.WORKING &&
216
+ model.hasConflict !== true;
217
+ const serverPath = React.useMemo(() => { var _a; return PathExt.join((_a = model.repositoryPath) !== null && _a !== void 0 ? _a : '', model.filename); }, [model.repositoryPath, model.filename]);
218
+ const hunkDiscard = React.useMemo(() => ({
219
+ enabled: canDiscardHunk,
220
+ save: async (text) => {
221
+ await contentsManager.save(serverPath, {
222
+ type: 'file',
223
+ format: 'text',
224
+ content: text
225
+ });
226
+ },
227
+ onAfterSave: () => {
228
+ // jupyterlab-git's panel auto-refreshes off the contents
229
+ // `fileChanged` signal the save above emits; we just need to
230
+ // re-pull so the diff reflects the reverted hunk. (We do not
231
+ // auto-close: jupyterlab-git owns the diff tab.)
232
+ void widget.refresh();
233
+ }
234
+ }), [canDiscardHunk, contentsManager, serverPath, widget]);
235
+ return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: false, oldText: state.oldText, newText: state.newText, newName: model.filename, oldName: model.filename, dark: dark, rendermime: rendermime, notebookViewMode: notebookViewMode, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, hunkDiscard: hunkDiscard }));
236
+ }
237
+ /**
238
+ * Notebook/JSON toggle mounted into the diff toolbar that jupyterlab-git
239
+ * passes to the factory. Bound to the widget's signals so it stays in sync
240
+ * with the rendered surface and only appears for parseable notebooks.
241
+ */
242
+ class NotebookViewModeToolbarItem extends ReactWidget {
243
+ constructor(widget) {
244
+ super();
245
+ this._widget = widget;
246
+ this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
247
+ }
248
+ render() {
249
+ return React.createElement(ToolbarControl, { widget: this._widget });
250
+ }
251
+ }
252
+ function ToolbarControl(props) {
253
+ const { widget } = props;
254
+ const [mode, setMode] = React.useState(() => widget.notebookViewMode);
255
+ const [available, setAvailable] = React.useState(() => widget.hasNotebookView);
256
+ React.useEffect(() => {
257
+ const onMode = (_sender, next) => {
258
+ setMode(next);
259
+ };
260
+ const onAvailable = (_sender, next) => {
261
+ setAvailable(next);
262
+ };
263
+ widget.notebookViewModeChanged.connect(onMode);
264
+ widget.hasNotebookViewChanged.connect(onAvailable);
265
+ setMode(widget.notebookViewMode);
266
+ setAvailable(widget.hasNotebookView);
267
+ return () => {
268
+ widget.notebookViewModeChanged.disconnect(onMode);
269
+ widget.hasNotebookViewChanged.disconnect(onAvailable);
270
+ };
271
+ }, [widget]);
272
+ return (React.createElement(NotebookViewModeControl, { mode: mode, available: available, onChange: next => widget.setNotebookViewMode(next) }));
273
+ }
274
+ /**
275
+ * Build the `Git.Diff.Factory` xtralab registers with jupyterlab-git. The
276
+ * factory closes over the application-level context (contents manager for
277
+ * hunk discard, rendermime for notebook outputs, theme manager) that the
278
+ * `Git.Diff.IFactoryOptions` does not carry.
279
+ */
280
+ function makeXtralabDiffFactory(context) {
281
+ return async (options) => {
282
+ const widget = new XtralabDiffWidget(options.model, context);
283
+ if (options.toolbar) {
284
+ options.toolbar.addItem('xtralab-notebook-view-mode', new NotebookViewModeToolbarItem(widget));
285
+ }
286
+ return widget;
287
+ };
288
+ }
289
+ /**
290
+ * Registers xtralab's diff rendering as jupyterlab-git's diff providers:
291
+ * `@pierre/diffs` for text and notebooks, the bundled `<img>`-based view
292
+ * for raster images.
293
+ */
294
+ const diffProviderPlugin = {
295
+ id: 'xtralab:git-diff-providers',
296
+ description: "Replaces jupyterlab-git's notebook, text and image diff plugins with xtralab's renderer.",
297
+ autoStart: true,
298
+ requires: [IGitExtension],
299
+ optional: [IRenderMimeRegistry, IThemeManager],
300
+ activate: (app, gitExtension, rendermime, themeManager) => {
301
+ const factory = makeXtralabDiffFactory({
302
+ contentsManager: app.serviceManager.contents,
303
+ rendermime,
304
+ themeManager
305
+ });
306
+ // Notebooks and raster images get extension-specific providers; every
307
+ // other text file falls back to the same factory. One factory serves
308
+ // all three — the surface auto-detects notebooks/images by filename.
309
+ gitExtension.registerDiffProvider('XtralabNotebook', ['.ipynb'], factory);
310
+ gitExtension.registerDiffProvider('XtralabImage', IMAGE_DIFF_EXTENSIONS, factory);
311
+ gitExtension.registerFallbackDiffProvider(factory);
312
+ }
313
+ };
314
+ export default diffProviderPlugin;
@@ -0,0 +1,115 @@
1
+ import * as React from 'react';
2
+ import { IThemeManager } from '@jupyterlab/apputils';
3
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
+ /**
5
+ * Shared diff rendering surface used by both diff hosts in this extension:
6
+ *
7
+ * - the launcher dashboard's own `xtralab:git:open-diff` command
8
+ * (via {@link DiffContentWidget} in `diffWidget.tsx`), and
9
+ * - the `jupyterlab-git` diff providers registered into the upstream git
10
+ * panel (via `XtralabDiffWidget` in `diffProvider.tsx`).
11
+ *
12
+ * The surface is deliberately content-source agnostic: callers resolve the
13
+ * old/new text however they like (xtralab's REST helper, or a
14
+ * `Git.Diff.IModel`'s `content()` getters) and hand the resolved strings in.
15
+ * Everything visual — the `@pierre/diffs` split view, the cell-by-cell
16
+ * notebook view, the draggable column resizer, and the per-hunk discard
17
+ * affordance — lives here so the two hosts stay pixel-identical.
18
+ */
19
+ /**
20
+ * The CSS class added to the diff main-area widget. The selectors that
21
+ * style the embedded `@pierre/diffs` viewer hang off this class.
22
+ */
23
+ export declare const DIFF_WIDGET_CSS_CLASS = "jp-xtralab-DiffWidget";
24
+ /**
25
+ * View modes available for `.ipynb` diffs. `notebook` is the cell-by-cell
26
+ * rendered view; `json` is the raw nbformat JSON file diff, for inspecting
27
+ * exactly which bytes changed.
28
+ */
29
+ export type NotebookDiffViewMode = 'notebook' | 'json';
30
+ export declare function readStoredNotebookViewMode(): NotebookDiffViewMode;
31
+ export declare function writeStoredNotebookViewMode(mode: NotebookDiffViewMode): void;
32
+ /**
33
+ * Decide whether the active JupyterLab theme is a dark theme. Prefers the
34
+ * `IThemeManager` token when available, and falls back to sniffing the
35
+ * `data-jp-theme-light` attribute that JupyterLab sets on `<body>` so the
36
+ * component still works in stripped-down hosts where the token isn't
37
+ * provided (lite, certain notebook configurations, …).
38
+ */
39
+ export declare function isDarkTheme(themeManager: IThemeManager | null): boolean;
40
+ /**
41
+ * Optional per-hunk discard wiring. When supplied and `enabled`, each hunk
42
+ * in a plain-text/code diff gets an inline "discard" button; clicking it
43
+ * reverts that hunk and hands the rebuilt full-file text back to the host
44
+ * via {@link IHunkDiscard.save}. The host owns the actual write (it knows
45
+ * the on-disk path) and triggers any follow-up refresh in
46
+ * {@link IHunkDiscard.onAfterSave}.
47
+ */
48
+ export interface IHunkDiscard {
49
+ enabled: boolean;
50
+ save: (fullText: string) => Promise<void>;
51
+ onAfterSave: () => void;
52
+ }
53
+ export interface IDiffSurfaceProps {
54
+ /** Whether the host is still resolving the file contents. */
55
+ loading: boolean;
56
+ /** Fatal error message to show instead of a diff, or `null`. */
57
+ error: string | null;
58
+ /** Whether the file is binary (no textual diff is rendered). */
59
+ isBinary: boolean;
60
+ /** Resolved old/reference text. Ignored while loading/binary/errored. */
61
+ oldText: string;
62
+ /** Resolved new/challenger text. Ignored while loading/binary/errored. */
63
+ newText: string;
64
+ /**
65
+ * New-side path. Drives notebook detection and the `@pierre/diffs` file
66
+ * name on the additions side.
67
+ */
68
+ newName: string;
69
+ /**
70
+ * Old-side path. Differs from {@link newName} only for renames; defaults
71
+ * to {@link newName} when the host does not track a previous path.
72
+ */
73
+ oldName?: string;
74
+ /** Whether to render with the dark `@pierre/diffs` theme. */
75
+ dark: boolean;
76
+ /**
77
+ * Rendermime registry used by the notebook view to render outputs and
78
+ * markdown cells. May be `null` in stripped-down hosts; the notebook
79
+ * diff falls back to a textual representation in that case.
80
+ */
81
+ rendermime: IRenderMimeRegistry | null;
82
+ /** Current rendered-vs-JSON choice for notebook diffs (host-controlled). */
83
+ notebookViewMode: NotebookDiffViewMode;
84
+ /**
85
+ * Called whenever the availability of a rendered notebook view changes,
86
+ * so the host can show/hide its Notebook/JSON toolbar toggle.
87
+ */
88
+ onNotebookAvailabilityChange?: (available: boolean) => void;
89
+ /**
90
+ * Called after each (re)computation of the diff with the number of hunks
91
+ * (or `null` when no textual diff exists). Hosts use this to implement
92
+ * post-discard behaviors such as auto-closing an emptied diff.
93
+ */
94
+ onMetadataChange?: (info: {
95
+ hunkCount: number | null;
96
+ }) => void;
97
+ /** Optional per-hunk discard wiring; omit for a read-only diff. */
98
+ hunkDiscard?: IHunkDiscard;
99
+ }
100
+ /**
101
+ * The shared, content-source-agnostic diff renderer. See the module
102
+ * docstring for the division of labor between this surface and its hosts.
103
+ */
104
+ export declare function DiffSurface(props: IDiffSurfaceProps): React.ReactElement;
105
+ /**
106
+ * Segmented Notebook/JSON selector. Hosts mount this into whatever toolbar
107
+ * they own (the launcher's `MainAreaWidget` toolbar, or the
108
+ * `jupyterlab-git`-provided diff toolbar) and drive its value/visibility
109
+ * from the same state they pass to {@link DiffSurface}.
110
+ */
111
+ export declare function NotebookViewModeControl(props: {
112
+ mode: NotebookDiffViewMode;
113
+ available: boolean;
114
+ onChange: (mode: NotebookDiffViewMode) => void;
115
+ }): React.ReactElement;