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.
- package/README.md +58 -33
- package/lib/fileBrowser/commands.d.ts +1 -0
- package/lib/fileBrowser/commands.js +46 -1
- package/lib/fileBrowser/fileBrowser.js +4 -8
- package/lib/fileBrowser/gitStatus.d.ts +16 -5
- package/lib/fileBrowser/gitStatus.js +28 -17
- package/lib/fileBrowser/widget.d.ts +15 -0
- package/lib/fileBrowser/widget.js +13 -0
- package/lib/git/commands.d.ts +5 -0
- package/lib/git/commands.js +4 -3
- package/lib/git/diffPreferences.d.ts +90 -0
- package/lib/git/diffPreferences.js +107 -0
- package/lib/git/diffProvider.js +10 -2
- package/lib/git/diffSurface.d.ts +34 -19
- package/lib/git/diffSurface.js +49 -115
- package/lib/git/diffTheme.d.ts +7 -0
- package/lib/git/diffTheme.js +23 -0
- package/lib/git/diffWidget.d.ts +7 -30
- package/lib/git/diffWidget.js +56 -100
- package/lib/git/imageDiff.d.ts +9 -0
- package/lib/git/imageDiff.js +2 -28
- package/lib/git/index.js +41 -2
- package/lib/git/notebookDiff.js +3 -2
- package/package.json +5 -3
- package/schema/git-diff-preferences.json +42 -0
- package/src/fileBrowser/commands.ts +64 -0
- package/src/fileBrowser/fileBrowser.tsx +4 -9
- package/src/fileBrowser/gitStatus.ts +33 -20
- package/src/fileBrowser/widget.tsx +22 -0
- package/src/git/commands.ts +13 -1
- package/src/git/diffPreferences.ts +192 -0
- package/src/git/diffProvider.tsx +11 -2
- package/src/git/diffSurface.tsx +113 -146
- package/src/git/diffTheme.ts +24 -0
- package/src/git/diffWidget.tsx +108 -138
- package/src/git/imageDiff.tsx +12 -34
- package/src/git/index.ts +51 -2
- package/src/git/notebookDiff.tsx +3 -2
- 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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
agent-ready JupyterLab base.
|
|
10
|
-
|
|
11
|
-
, 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
|
+

|
|
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
|
|
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
|
-
|
|
33
|
+
show how to install xtralab as a uv tool or add it to a project.
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
app
|
|
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
|
-
|
|
42
|
+
### Agent launcher
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
prompt
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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 {
|
|
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
|
|
101
|
+
const changes = await loadGitChanges(GIT_REPO_PATH);
|
|
107
102
|
if (cancelled) {
|
|
108
103
|
return;
|
|
109
104
|
}
|
|
110
|
-
gitStatusEntries =
|
|
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
|
-
*
|
|
4
|
-
*
|
|
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
|
|
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
|
|
24
|
-
* Any failure yields an empty array — the file browser
|
|
25
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
*/
|
package/lib/git/commands.d.ts
CHANGED
|
@@ -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
|
*/
|
package/lib/git/commands.js
CHANGED
|
@@ -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;
|