xtralab 0.15.1 → 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 (39) hide show
  1. package/README.md +58 -33
  2. package/lib/fileBrowser/commands.d.ts +1 -0
  3. package/lib/fileBrowser/commands.js +46 -1
  4. package/lib/fileBrowser/fileBrowser.js +4 -8
  5. package/lib/fileBrowser/gitStatus.d.ts +16 -5
  6. package/lib/fileBrowser/gitStatus.js +28 -17
  7. package/lib/fileBrowser/widget.d.ts +15 -0
  8. package/lib/fileBrowser/widget.js +13 -0
  9. package/lib/git/commands.d.ts +5 -0
  10. package/lib/git/commands.js +4 -3
  11. package/lib/git/diffPreferences.d.ts +90 -0
  12. package/lib/git/diffPreferences.js +107 -0
  13. package/lib/git/diffProvider.js +10 -2
  14. package/lib/git/diffSurface.d.ts +34 -19
  15. package/lib/git/diffSurface.js +49 -115
  16. package/lib/git/diffTheme.d.ts +7 -0
  17. package/lib/git/diffTheme.js +23 -0
  18. package/lib/git/diffWidget.d.ts +7 -30
  19. package/lib/git/diffWidget.js +56 -100
  20. package/lib/git/imageDiff.d.ts +9 -0
  21. package/lib/git/imageDiff.js +2 -28
  22. package/lib/git/index.js +41 -2
  23. package/lib/git/notebookDiff.js +3 -2
  24. package/package.json +5 -3
  25. package/schema/git-diff-preferences.json +42 -0
  26. package/src/fileBrowser/commands.ts +64 -0
  27. package/src/fileBrowser/fileBrowser.tsx +4 -9
  28. package/src/fileBrowser/gitStatus.ts +33 -20
  29. package/src/fileBrowser/widget.tsx +22 -0
  30. package/src/git/commands.ts +13 -1
  31. package/src/git/diffPreferences.ts +192 -0
  32. package/src/git/diffProvider.tsx +11 -2
  33. package/src/git/diffSurface.tsx +113 -146
  34. package/src/git/diffTheme.ts +24 -0
  35. package/src/git/diffWidget.tsx +108 -138
  36. package/src/git/imageDiff.tsx +12 -34
  37. package/src/git/index.ts +51 -2
  38. package/src/git/notebookDiff.tsx +3 -2
  39. package/style/git.css +4 -2
package/README.md CHANGED
@@ -2,19 +2,18 @@
2
2
 
3
3
  An opinionated JupyterLab meta-package for coding agents.
4
4
 
5
- xtralab reshapes JupyterLab around terminal coding agents. It bundles a curated
6
- set of extensions, a path-first file browser, rich git diffs, an agent launcher,
7
- a Model Context Protocol server that lets agents drive the app, and a quieter
8
- set of defaults. It builds on [`ajlab`](https://github.com/jtpio/ajlab), the
9
- agent-ready JupyterLab base.
10
-
11
- ![The xtralab workspace: path-first file browser, agent launcher, a side-by-side git diff, and a running Claude Code session](./images/hero.webp)
12
-
13
- The [documentation](https://jtpio.github.io/xtralab/) covers everything in
14
- detail: [installation](https://jtpio.github.io/xtralab/installation/), a
15
- [tour of the features](https://jtpio.github.io/xtralab/getting-started/),
16
- [connecting agents over MCP](https://jtpio.github.io/xtralab/agents/mcp/),
17
- [agent skills](https://jtpio.github.io/xtralab/agents/skills/), and the
5
+ xtralab installs JupyterLab with a set of extensions and defaults for work
6
+ with terminal coding agents: an agent launcher, side-by-side git diffs for
7
+ text files and notebooks, prompts about selected code, and a Model Context
8
+ Protocol server that lets agents drive the app. It builds on
9
+ [`ajlab`](https://github.com/jtpio/ajlab), the agent-ready JupyterLab base.
10
+
11
+ ![The xtralab workspace: file browser, agent launcher, a side-by-side git diff, and a running Claude Code session](./images/hero.webp)
12
+
13
+ See the [documentation](https://jtpio.github.io/xtralab/) for the
14
+ [installation](https://jtpio.github.io/xtralab/installation/), a
15
+ [getting started tutorial](https://jtpio.github.io/xtralab/getting-started/),
16
+ the [features](https://jtpio.github.io/xtralab/features/launcher/), and the
18
17
  [desktop app](https://jtpio.github.io/xtralab/desktop/).
19
18
 
20
19
  ## Install
@@ -24,34 +23,60 @@ pip install xtralab
24
23
  jupyter lab
25
24
  ```
26
25
 
27
- To try it from any directory without setting up an environment first, uv can
28
- build a throwaway one:
26
+ To run xtralab in any folder without installing it, use uv:
29
27
 
30
28
  ```bash
31
29
  uvx --from xtralab jupyter-lab
32
30
  ```
33
31
 
34
32
  The [installation docs](https://jtpio.github.io/xtralab/installation/) also
35
- cover installing xtralab on your PATH or adding it to a project.
33
+ show how to install xtralab as a uv tool or add it to a project.
36
34
 
37
- A standalone desktop app (DMG on macOS, AppImage on Linux) ships with every
38
- tagged release on the
39
- [releases page](https://github.com/jtpio/xtralab/releases/latest). The macOS
40
- app is signed and notarized and keeps itself up to date; see the
41
- [desktop app docs](https://jtpio.github.io/xtralab/desktop/) for how it
42
- manages projects and kernels.
35
+ The desktop app (DMG on macOS, AppImage on Linux) is on the
36
+ [releases page](https://github.com/jtpio/xtralab/releases/latest). It
37
+ includes its own Python runtime. See the
38
+ [desktop app docs](https://jtpio.github.io/xtralab/desktop/).
43
39
 
44
40
  ## Highlights
45
41
 
46
- ![The xtralab launcher](./images/launcher.webp)
42
+ ### Agent launcher
47
43
 
48
- The launcher starts any agent installed on your machine, with an optional
49
- prompt and a **Changes** list that jumps straight into a side-by-side git diff
50
- you can edit in place. Select code in an editor, a notebook, or a diff to send
51
- a prompt about it to a new or already-running agent, or queue prompts while
52
- you read. One omnibox searches files and commands, the Terminals panel shows
53
- which agent runs where, and quieter defaults keep the workspace calm. The
54
- [documentation](https://jtpio.github.io/xtralab/) walks through each of these.
44
+ Start any coding agent installed on your machine, with an optional first
45
+ prompt. The **Changes** list opens the diff of each changed file.
46
+ [Read more](https://jtpio.github.io/xtralab/features/launcher/).
47
+
48
+ <img src="./images/launcher.webp" width="720" alt="The launcher: a prompt box, one button per installed agent, the Open section, and the list of changed files" />
49
+
50
+ ### Git diffs
51
+
52
+ Review changes side by side, for text files and for notebooks, and edit the
53
+ working copy in the diff.
54
+ [Read more](https://jtpio.github.io/xtralab/features/git-diffs/).
55
+
56
+ <img src="./images/diff.webp" width="1000" alt="A side-by-side diff of a Python file, with removed lines on the left and added lines on the right" />
57
+
58
+ ### Ask an agent
59
+
60
+ Select code, type an instruction, and send it to a new or running agent. The
61
+ prompt includes the file path, the line range, and the selected code.
62
+ [Read more](https://jtpio.github.io/xtralab/features/ask-agent/).
63
+
64
+ <img src="./images/ask-agent.webp" width="796" alt="The prompt box below a selected function, with an instruction and one button per agent" />
65
+
66
+ ### Omnibox
67
+
68
+ Search files and commands, or send a prompt to an agent, from one search box
69
+ (<kbd>Cmd/Ctrl</kbd>+<kbd>K</kbd>).
70
+ [Read more](https://jtpio.github.io/xtralab/features/omnibox/).
71
+
72
+ <img src="./images/omnibox.webp" width="642" alt="The omnibox with matching commands, a matching file, and one Ask row per agent" />
73
+
74
+ ### Terminals panel
75
+
76
+ See the agent in each terminal and its latest line of output.
77
+ [Read more](https://jtpio.github.io/xtralab/features/terminals/).
78
+
79
+ <img src="./images/terminals.webp" width="451" alt="The Terminals panel with three agent sessions and their latest output" />
55
80
 
56
81
  ## Connecting agents to JupyterLab (MCP)
57
82
 
@@ -64,9 +89,9 @@ xtralab:
64
89
  claude mcp add jupyter -- jupyter-server-mcp-proxy
65
90
  ```
66
91
 
67
- The launcher surfaces the matching command for every agent you have
68
- installed, and the [MCP docs page](https://jtpio.github.io/xtralab/agents/mcp/)
69
- covers the details.
92
+ The launcher shows this command for each installed agent. See the
93
+ [MCP docs](https://jtpio.github.io/xtralab/agents/mcp/) for Codex, GitHub
94
+ Copilot, and other agents.
70
95
 
71
96
  [mcp]: https://github.com/jupyter-ai-contrib/jupyter-server-mcp
72
97
  [mcp-spec]: https://modelcontextprotocol.io
@@ -11,6 +11,7 @@ import { IXtralabFileBrowser } from './widget';
11
11
  export declare namespace CommandIDs {
12
12
  const open = "xtralab:open";
13
13
  const openBrowserTab = "xtralab:open-browser-tab";
14
+ const openGitDiff = "xtralab:open-git-diff";
14
15
  const rename = "xtralab:rename";
15
16
  const del = "xtralab:delete";
16
17
  const duplicate = "xtralab:duplicate";
@@ -1,8 +1,10 @@
1
1
  import { Clipboard, Dialog, InputDialog, showDialog, showErrorMessage } from '@jupyterlab/apputils';
2
2
  import { PageConfig, PathExt } from '@jupyterlab/coreutils';
3
3
  import { nullTranslator } from '@jupyterlab/translation';
4
- import { addIcon, closeIcon, collapseAllIcon, CommandToolbarButton, copyIcon, downloadIcon, editIcon, fileIcon, filterIcon, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
4
+ import { addIcon, closeIcon, collapseAllIcon, CommandToolbarButton, copyIcon, downloadIcon, editIcon, fileIcon, filterIcon, LabIcon, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
5
+ import { CommandIDs as GitCommandIDs } from '../git/commands';
5
6
  import { toCanonicalPath, toServerPath } from './contents';
7
+ import { GIT_REPO_PATH } from './gitStatus';
6
8
  import { FILE_BROWSER_ID } from './widget';
7
9
  /**
8
10
  * Command ids for the xtralab browser. Namespaced `xtralab:` rather than
@@ -13,6 +15,7 @@ export var CommandIDs;
13
15
  (function (CommandIDs) {
14
16
  CommandIDs.open = 'xtralab:open';
15
17
  CommandIDs.openBrowserTab = 'xtralab:open-browser-tab';
18
+ CommandIDs.openGitDiff = 'xtralab:open-git-diff';
16
19
  CommandIDs.rename = 'xtralab:rename';
17
20
  CommandIDs.del = 'xtralab:delete';
18
21
  CommandIDs.duplicate = 'xtralab:duplicate';
@@ -66,6 +69,19 @@ function getOpenPaths(app, browser) {
66
69
  }
67
70
  return candidates.filter(path => !path.endsWith('/'));
68
71
  }
72
+ /**
73
+ * Git changes of the target file, at most one staged and one unstaged.
74
+ * Status paths are repo-relative; like the tree's git badges, this assumes
75
+ * the repo root is the server root.
76
+ */
77
+ function getTargetChanges(app, browser) {
78
+ const targetPath = getTargetPath(app, browser);
79
+ if (targetPath === undefined) {
80
+ return [];
81
+ }
82
+ const serverPath = toServerPath(targetPath);
83
+ return browser.gitChanges.filter(change => change.path === serverPath);
84
+ }
69
85
  function hasTarget(app, browser) {
70
86
  return getTargetPath(app, browser) !== undefined;
71
87
  }
@@ -148,6 +164,16 @@ function makeOpenWithUpdater(app, browser) {
148
164
  args: { factory: factory.name, label: factory.label || factory.name },
149
165
  command: CommandIDs.open
150
166
  }));
167
+ const changes = getTargetChanges(app, browser);
168
+ if (changes.length > 0) {
169
+ items.push(submenu.addItem({ type: 'separator' }));
170
+ for (const change of changes) {
171
+ items.push(submenu.addItem({
172
+ args: { group: change.group },
173
+ command: CommandIDs.openGitDiff
174
+ }));
175
+ }
176
+ }
151
177
  };
152
178
  }
153
179
  /**
@@ -204,6 +230,25 @@ export function registerCommands(opts) {
204
230
  });
205
231
  }
206
232
  });
233
+ commands.addCommand(CommandIDs.openGitDiff, {
234
+ label: args => args.group === 'staged'
235
+ ? trans.__('Git Diff (Staged)')
236
+ : trans.__('Git Diff (Working)'),
237
+ icon: LabIcon.resolve({ icon: 'git:diff' }).bindprops({
238
+ stylesheet: 'menuItem'
239
+ }),
240
+ execute: async (args) => {
241
+ const change = getTargetChanges(app, browser).find(candidate => candidate.group === args.group);
242
+ if (change === undefined) {
243
+ return;
244
+ }
245
+ const diffArgs = {
246
+ repoPath: GIT_REPO_PATH,
247
+ change
248
+ };
249
+ return commands.execute(GitCommandIDs.openDiff, diffArgs);
250
+ }
251
+ });
207
252
  commands.addCommand(CommandIDs.rename, {
208
253
  label: trans.__('Rename…'),
209
254
  icon: editIcon.bindprops({ stylesheet: 'menuItem' }),
@@ -7,7 +7,7 @@ import { FileTree, useFileTree } from '@pierre/trees/react';
7
7
  import { ROOT_LOAD_KEY, listDirectory, parentOf, toServerPath } from './contents';
8
8
  import { computeDropMoves, computeRootDropMoves, createTreeDragAndDropConfig, useRootDropZone } from './dragAndDrop';
9
9
  import { buildIgnoredEntries, loadGitignoreMatcher } from './gitignore';
10
- import { loadGitStatusEntries } from './gitStatus';
10
+ import { GIT_REPO_PATH, loadGitChanges, toGitStatusEntries } from './gitStatus';
11
11
  import { FILE_BROWSER_ICONS } from './icons';
12
12
  /**
13
13
  * Git status poll cadence, aligned with the git panel's polling so both
@@ -21,11 +21,6 @@ const GIT_STATUS_POLL_MAX_MS = 300000;
21
21
  */
22
22
  const FILE_LISTING_REFRESH_INTERVAL_MS = 10000;
23
23
  const FILE_LISTING_REFRESH_MAX_MS = 300000;
24
- /**
25
- * Repo path for `/git/*` calls; empty means the server root, letting git
26
- * resolve the enclosing repo (same convention as the git panel).
27
- */
28
- const GIT_REPO_PATH = '';
29
24
  const FILE_TREE_TAG = 'file-tree-container';
30
25
  /**
31
26
  * Injected into the tree's shadow root, where outside CSS cannot reach.
@@ -103,11 +98,12 @@ export function FileBrowserComponent(props) {
103
98
  syncGitStatus();
104
99
  };
105
100
  const refreshGitStatus = async () => {
106
- const next = await loadGitStatusEntries(GIT_REPO_PATH);
101
+ const changes = await loadGitChanges(GIT_REPO_PATH);
107
102
  if (cancelled) {
108
103
  return;
109
104
  }
110
- gitStatusEntries = next;
105
+ gitStatusEntries = toGitStatusEntries(changes);
106
+ widget === null || widget === void 0 ? void 0 : widget.updateGitChanges(changes);
111
107
  syncGitStatus();
112
108
  };
113
109
  const fetchDirectory = async (canonicalPath) => {
@@ -1,8 +1,19 @@
1
1
  import type { GitStatusEntry } from '@pierre/trees';
2
+ import type { IFileChange } from '../git/tokens';
2
3
  /**
3
- * Fetch the porcelain status for `repoPath` as `GitStatusEntry` values.
4
- * Any failure yields an empty array — the file browser must work without
5
- * git. A file both staged and unstaged gets one entry, the unstaged status
6
- * winning (mirrors what is on disk, matching VS Code).
4
+ * Repo path for `/git/*` calls; empty means the server root, letting git
5
+ * resolve the enclosing repo (same convention as the git panel).
7
6
  */
8
- export declare function loadGitStatusEntries(repoPath: string): Promise<GitStatusEntry[]>;
7
+ export declare const GIT_REPO_PATH = "";
8
+ /**
9
+ * Fetch the porcelain status for `repoPath`, one entry per staged or
10
+ * unstaged change. Any failure yields an empty array — the file browser
11
+ * must work without git.
12
+ */
13
+ export declare function loadGitChanges(repoPath: string): Promise<IFileChange[]>;
14
+ /**
15
+ * Collapse changes into the tree's `GitStatusEntry` values. A file both
16
+ * staged and unstaged gets one entry, the unstaged status winning (mirrors
17
+ * what is on disk, matching VS Code).
18
+ */
19
+ export declare function toGitStatusEntries(changes: readonly IFileChange[]): GitStatusEntry[];
@@ -1,4 +1,9 @@
1
1
  import { expandStatusFiles, status } from '../git/api';
2
+ /**
3
+ * Repo path for `/git/*` calls; empty means the server root, letting git
4
+ * resolve the enclosing repo (same convention as the git panel).
5
+ */
6
+ export const GIT_REPO_PATH = '';
2
7
  /**
3
8
  * Translate a `jupyterlab_git` porcelain status into a `@pierre/trees`
4
9
  * `GitStatus`. The tree only supports the six common statuses, so the rare
@@ -20,31 +25,37 @@ function toTreeStatus(value) {
20
25
  }
21
26
  }
22
27
  /**
23
- * Fetch the porcelain status for `repoPath` as `GitStatusEntry` values.
24
- * Any failure yields an empty array — the file browser must work without
25
- * git. A file both staged and unstaged gets one entry, the unstaged status
26
- * winning (mirrors what is on disk, matching VS Code).
28
+ * Fetch the porcelain status for `repoPath`, one entry per staged or
29
+ * unstaged change. Any failure yields an empty array — the file browser
30
+ * must work without git.
27
31
  */
28
- export async function loadGitStatusEntries(repoPath) {
32
+ export async function loadGitChanges(repoPath) {
29
33
  try {
30
34
  const result = await status(repoPath);
31
35
  if (result.code !== 0) {
32
36
  return [];
33
37
  }
34
- const changes = expandStatusFiles(result.files);
35
- const byPath = new Map();
36
- for (const change of changes) {
37
- if (change.group === 'unstaged' || !byPath.has(change.path)) {
38
- byPath.set(change.path, change.status);
39
- }
40
- }
41
- const entries = [];
42
- byPath.forEach((value, path) => {
43
- entries.push({ path, status: toTreeStatus(value) });
44
- });
45
- return entries;
38
+ return expandStatusFiles(result.files);
46
39
  }
47
40
  catch (_a) {
48
41
  return [];
49
42
  }
50
43
  }
44
+ /**
45
+ * Collapse changes into the tree's `GitStatusEntry` values. A file both
46
+ * staged and unstaged gets one entry, the unstaged status winning (mirrors
47
+ * what is on disk, matching VS Code).
48
+ */
49
+ export function toGitStatusEntries(changes) {
50
+ const byPath = new Map();
51
+ for (const change of changes) {
52
+ if (change.group === 'unstaged' || !byPath.has(change.path)) {
53
+ byPath.set(change.path, change.status);
54
+ }
55
+ }
56
+ const entries = [];
57
+ byPath.forEach((value, path) => {
58
+ entries.push({ path, status: toTreeStatus(value) });
59
+ });
60
+ return entries;
61
+ }
@@ -5,6 +5,7 @@ import { ITranslator } from '@jupyterlab/translation';
5
5
  import { Toolbar } from '@jupyterlab/ui-components';
6
6
  import { ISignal } from '@lumino/signaling';
7
7
  import { AccordionPanel, Widget } from '@lumino/widgets';
8
+ import type { IFileChange } from '../git/tokens';
8
9
  export declare const FILE_BROWSER_ID = "xtralab:file-browser";
9
10
  /**
10
11
  * The options used to create an {@link XtralabFileBrowser}.
@@ -51,6 +52,11 @@ export interface IXtralabFileBrowser {
51
52
  * Emits when the tree's selection changes.
52
53
  */
53
54
  readonly selectionChanged: ISignal<IXtralabFileBrowser, readonly string[]>;
55
+ /**
56
+ * Git changes from the latest status poll, one per staged or unstaged
57
+ * change; paths are repo-relative.
58
+ */
59
+ readonly gitChanges: readonly IFileChange[];
54
60
  /**
55
61
  * Emits when {@link refresh} is called.
56
62
  */
@@ -142,6 +148,10 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
142
148
  * A signal emitted when the tree's selection changes.
143
149
  */
144
150
  get selectionChanged(): ISignal<this, readonly string[]>;
151
+ /**
152
+ * Git changes from the latest status poll.
153
+ */
154
+ get gitChanges(): readonly IFileChange[];
145
155
  /**
146
156
  * A signal emitted when {@link refresh} is called.
147
157
  */
@@ -174,6 +184,10 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
174
184
  * Update the cached selection from the React tree.
175
185
  */
176
186
  updateSelection(paths: readonly string[]): void;
187
+ /**
188
+ * Update the cached git changes from the React tree's status poll.
189
+ */
190
+ updateGitChanges(changes: readonly IFileChange[]): void;
177
191
  /**
178
192
  * Trigger a refresh of every loaded directory in the tree.
179
193
  */
@@ -267,6 +281,7 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
267
281
  private _translator;
268
282
  private _selectedPaths;
269
283
  private _selectionChanged;
284
+ private _gitChanges;
270
285
  private _refreshRequested;
271
286
  private _pathAdded;
272
287
  private _revealRequested;
@@ -29,6 +29,7 @@ export class XtralabFileBrowser extends Widget {
29
29
  super();
30
30
  this._selectedPaths = [];
31
31
  this._selectionChanged = new Signal(this);
32
+ this._gitChanges = [];
32
33
  this._refreshRequested = new Signal(this);
33
34
  this._pathAdded = new Signal(this);
34
35
  this._revealRequested = new Signal(this);
@@ -92,6 +93,12 @@ export class XtralabFileBrowser extends Widget {
92
93
  get selectionChanged() {
93
94
  return this._selectionChanged;
94
95
  }
96
+ /**
97
+ * Git changes from the latest status poll.
98
+ */
99
+ get gitChanges() {
100
+ return this._gitChanges;
101
+ }
95
102
  /**
96
103
  * A signal emitted when {@link refresh} is called.
97
104
  */
@@ -141,6 +148,12 @@ export class XtralabFileBrowser extends Widget {
141
148
  this._selectedPaths = paths;
142
149
  this._selectionChanged.emit(paths);
143
150
  }
151
+ /**
152
+ * Update the cached git changes from the React tree's status poll.
153
+ */
154
+ updateGitChanges(changes) {
155
+ this._gitChanges = changes;
156
+ }
144
157
  /**
145
158
  * Trigger a refresh of every loaded directory in the tree.
146
159
  */
@@ -5,6 +5,7 @@ import { Contents } from '@jupyterlab/services';
5
5
  import type { TranslationBundle } from '@jupyterlab/translation';
6
6
  import { ISignal } from '@lumino/signaling';
7
7
  import type { IAskAgent } from '../askAgent/tokens';
8
+ import type { IDiffPreferences } from './diffPreferences';
8
9
  import { XtralabDiffWidget } from './diffWidget';
9
10
  import { IFileChange } from './tokens';
10
11
  /**
@@ -94,6 +95,10 @@ interface IRegisterGitCommandsOptions {
94
95
  * Ask-agent popup for prompting an agent about diff lines; may be `null`.
95
96
  */
96
97
  askAgent: IAskAgent | null;
98
+ /**
99
+ * The display choices shared by all diff views.
100
+ */
101
+ preferences: IDiffPreferences;
97
102
  /**
98
103
  * Translation bundle for user-facing strings.
99
104
  */
@@ -76,9 +76,9 @@ export class DiffMainAreaWidget extends MainAreaWidget {
76
76
  }
77
77
  function createDiffWidget(options) {
78
78
  var _a;
79
- const { repoPath, change, themeManager, contentsManager, rendermime, askAgent, trans } = options;
79
+ const { repoPath, change, themeManager, contentsManager, rendermime, askAgent, preferences, trans } = options;
80
80
  const pinned = options.pinned === true;
81
- const content = new XtralabDiffWidget(fileChangeToDiffModel(repoPath, change), { contentsManager, rendermime, themeManager, askAgent, trans });
81
+ const content = new XtralabDiffWidget(fileChangeToDiffModel(repoPath, change), { contentsManager, rendermime, themeManager, askAgent, preferences, trans });
82
82
  const widget = new DiffMainAreaWidget({ content }, change, pinned, trans);
83
83
  widget.id = pinned ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
84
84
  widget.title.label = formatTitle(change, trans);
@@ -145,7 +145,7 @@ function createDiffWidget(options) {
145
145
  * Register the launcher's git diff command.
146
146
  */
147
147
  export function registerGitCommands(options) {
148
- const { app, themeManager, contentsManager, rendermime, askAgent, trans, trackDiff, findDiff, onPinned } = options;
148
+ const { app, themeManager, contentsManager, rendermime, askAgent, preferences, trans, trackDiff, findDiff, onPinned } = options;
149
149
  const { commands } = app;
150
150
  commands.addCommand(CommandIDs.openDiff, {
151
151
  label: trans.__('Open Git Diff'),
@@ -171,6 +171,7 @@ export function registerGitCommands(options) {
171
171
  contentsManager,
172
172
  rendermime,
173
173
  askAgent,
174
+ preferences,
174
175
  trans,
175
176
  pinned: pin,
176
177
  onPinned
@@ -0,0 +1,90 @@
1
+ import type { ISettingRegistry } from '@jupyterlab/settingregistry';
2
+ import type { IStateDB } from '@jupyterlab/statedb';
3
+ import { Token } from '@lumino/coreutils';
4
+ import { ISignal } from '@lumino/signaling';
5
+ export type DiffStyle = 'split' | 'unified';
6
+ export type DiffStyleControlMode = 'buttons' | 'toggle';
7
+ export type NotebookDiffViewMode = 'notebook' | 'json';
8
+ export type ImageDiffViewMode = '2-up' | 'swipe' | 'onion';
9
+ export declare const MIN_SPLIT_RATIO = 0.1;
10
+ export declare const MAX_SPLIT_RATIO = 0.9;
11
+ export declare const DEFAULT_SPLIT_RATIO = 0.5;
12
+ /**
13
+ * Display choices shared by every diff view. The toggles live in the
14
+ * plugin settings; the split width lives in the state database.
15
+ */
16
+ export interface IDiffPreferences {
17
+ /**
18
+ * The current values; a new object after each change.
19
+ */
20
+ readonly values: IDiffPreferences.IValues;
21
+ /**
22
+ * A signal emitted when any value changes.
23
+ */
24
+ readonly changed: ISignal<IDiffPreferences, void>;
25
+ /**
26
+ * Change some values and persist them.
27
+ */
28
+ update(values: Partial<IDiffPreferences.IValues>): void;
29
+ }
30
+ export declare namespace IDiffPreferences {
31
+ /**
32
+ * The diff display values.
33
+ */
34
+ interface IValues {
35
+ /**
36
+ * Split or unified layout for textual file diffs.
37
+ */
38
+ diffStyle: DiffStyle;
39
+ /**
40
+ * Two toolbar buttons for the layout, or one button that toggles it.
41
+ */
42
+ diffStyleControl: DiffStyleControlMode;
43
+ /**
44
+ * Whether long lines wrap instead of scrolling horizontally.
45
+ */
46
+ lineWrap: boolean;
47
+ /**
48
+ * Rendered or JSON view for notebook diffs.
49
+ */
50
+ notebookViewMode: NotebookDiffViewMode;
51
+ /**
52
+ * Comparison mode for image diffs.
53
+ */
54
+ imageViewMode: ImageDiffViewMode;
55
+ /**
56
+ * Width of the left pane in split view, as a fraction of the diff width.
57
+ */
58
+ splitRatio: number;
59
+ }
60
+ }
61
+ export declare const IDiffPreferences: Token<IDiffPreferences>;
62
+ /**
63
+ * {@link IDiffPreferences} backed by the settings registry and the state
64
+ * database. Without them it keeps the values in memory only.
65
+ */
66
+ export declare class DiffPreferences implements IDiffPreferences {
67
+ get values(): IDiffPreferences.IValues;
68
+ get changed(): ISignal<this, void>;
69
+ update(values: Partial<IDiffPreferences.IValues>): void;
70
+ /**
71
+ * Read the toggles from the plugin settings and follow their changes.
72
+ */
73
+ connectSettings(settings: ISettingRegistry.ISettings): void;
74
+ /**
75
+ * Restore the split width from the state database and save it there.
76
+ */
77
+ connectState(state: IStateDB): Promise<void>;
78
+ /**
79
+ * Merge values without persisting them; return whether one changed.
80
+ */
81
+ private _apply;
82
+ private _values;
83
+ private _changed;
84
+ private _settings;
85
+ private _state;
86
+ }
87
+ /**
88
+ * Re-render on every preference change and return the current values.
89
+ */
90
+ export declare function useDiffPreferences(preferences: IDiffPreferences): IDiffPreferences.IValues;