xtralab 0.1.2 → 0.2.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 +84 -0
- package/lib/fileBrowser/fileBrowser.js +71 -1
- package/lib/fileBrowser/gitignore.d.ts +29 -0
- package/lib/fileBrowser/gitignore.js +74 -0
- package/lib/git/api.d.ts +11 -0
- package/lib/git/api.js +19 -0
- package/lib/git/commands.d.ts +3 -1
- package/lib/git/commands.js +10 -4
- package/lib/git/diffWidget.d.ts +11 -0
- package/lib/git/diffWidget.js +90 -9
- package/lib/git/index.js +22 -6
- package/lib/git/panelComponent.d.ts +3 -1
- package/lib/git/panelComponent.js +6 -3
- package/lib/index.js +9 -1
- package/lib/launcher/agents.d.ts +78 -0
- package/lib/launcher/agents.js +168 -0
- package/lib/launcher/availability.d.ts +12 -0
- package/lib/launcher/availability.js +53 -0
- package/lib/launcher/commands.d.ts +22 -0
- package/lib/launcher/commands.js +89 -0
- package/lib/launcher/dashboard.d.ts +46 -0
- package/lib/launcher/dashboard.js +164 -0
- package/lib/launcher/icons.d.ts +33 -0
- package/lib/launcher/icons.js +113 -0
- package/lib/launcher/index.d.ts +23 -0
- package/lib/launcher/index.js +191 -0
- package/lib/launcher/invocation.d.ts +29 -0
- package/lib/launcher/invocation.js +42 -0
- package/lib/menus/index.d.ts +14 -0
- package/lib/menus/index.js +102 -0
- package/lib/sidebar/index.d.ts +18 -0
- package/lib/sidebar/index.js +155 -0
- package/lib/statusBar/index.d.ts +14 -0
- package/lib/statusBar/index.js +89 -0
- package/lib/statusBar/widget.d.ts +121 -0
- package/lib/statusBar/widget.js +237 -0
- package/package.json +10 -2
- package/schema/launcher.json +70 -0
- package/schema/sidebar.json +57 -0
- package/src/fileBrowser/fileBrowser.tsx +75 -1
- package/src/fileBrowser/gitignore.ts +86 -0
- package/src/git/api.ts +20 -0
- package/src/git/commands.ts +19 -5
- package/src/git/diffWidget.tsx +109 -13
- package/src/git/index.ts +31 -5
- package/src/git/panelComponent.tsx +17 -5
- package/src/index.ts +9 -1
- package/src/launcher/agents.ts +248 -0
- package/src/launcher/availability.ts +73 -0
- package/src/launcher/commands.ts +103 -0
- package/src/launcher/dashboard.tsx +356 -0
- package/src/launcher/icons.ts +123 -0
- package/src/launcher/index.ts +228 -0
- package/src/launcher/invocation.ts +45 -0
- package/src/menus/index.ts +124 -0
- package/src/sidebar/index.ts +195 -0
- package/src/statusBar/index.ts +115 -0
- package/src/statusBar/widget.tsx +318 -0
- package/style/git.css +4 -8
- package/style/index.css +2 -0
- package/style/index.js +2 -0
- package/style/launcher.css +323 -0
- package/style/statusBar.css +95 -0
package/README.md
CHANGED
|
@@ -35,6 +35,61 @@ override versions and Jupyter configuration in the usual ways.
|
|
|
35
35
|
`jupyterlab-git` server REST API and `@pierre/diffs`. The bundled
|
|
36
36
|
`@jupyterlab/git` frontend is disabled automatically so the two panels do
|
|
37
37
|
not coexist.
|
|
38
|
+
- An agent-focused launcher that replaces the stock JupyterLab launcher.
|
|
39
|
+
The launcher tab shows an optional initial-prompt textarea, a row of
|
|
40
|
+
agent buttons (Claude, Codex, Gemini, Copilot, Goose, OpenCode, Kiro,
|
|
41
|
+
and Mistral Vibe), and a collapsible list of changed files (each
|
|
42
|
+
clickable into the diff viewer) below them. Clicking an agent opens a
|
|
43
|
+
fresh terminal and runs the agent's CLI; if the prompt textarea is
|
|
44
|
+
non-empty, the prompt is shell-quoted and spliced into the launch line
|
|
45
|
+
for agents that accept one. Agents are filtered by a `which`-based
|
|
46
|
+
availability check at activation, so only the ones actually installed
|
|
47
|
+
on the machine show up.
|
|
48
|
+
|
|
49
|
+
### Customizing the launcher
|
|
50
|
+
|
|
51
|
+
The launcher reads `xtralab:launcher` settings from the JupyterLab settings
|
|
52
|
+
registry. Open the Settings Editor (`Settings → Settings Editor → xtralab
|
|
53
|
+
launcher`) and edit the `agents` array; xtralab merges your entries with
|
|
54
|
+
the defaults by `id`.
|
|
55
|
+
|
|
56
|
+
```jsonc
|
|
57
|
+
{
|
|
58
|
+
"agents": [
|
|
59
|
+
// Hide an agent you never want to see, even if it's installed:
|
|
60
|
+
{ "id": "kiro", "enabled": false },
|
|
61
|
+
|
|
62
|
+
// Override an agent's command — e.g. point Claude at a shell alias.
|
|
63
|
+
// `requireAvailable: false` keeps the card visible even though
|
|
64
|
+
// `which cl` returns nothing (interactive shells expand the alias).
|
|
65
|
+
{ "id": "claude", "command": "cl", "requireAvailable": false },
|
|
66
|
+
|
|
67
|
+
// Add a brand-new agent. `iconSvg` is optional; without it the card
|
|
68
|
+
// falls back to a generic terminal icon. `promptArgs: []` means the
|
|
69
|
+
// launcher's prompt textarea is appended as a positional argument.
|
|
70
|
+
{
|
|
71
|
+
"id": "aider",
|
|
72
|
+
"label": "Aider",
|
|
73
|
+
"command": "aider",
|
|
74
|
+
"promptArgs": []
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Field reference (every field except `id` is optional):
|
|
81
|
+
|
|
82
|
+
| Field | Effect |
|
|
83
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `id` | Built-in id to override, or a new id to define a new card. |
|
|
85
|
+
| `label` | Card title. |
|
|
86
|
+
| `caption` | Subtitle shown beneath the title. |
|
|
87
|
+
| `command` | Literal text typed into the new terminal. |
|
|
88
|
+
| `iconSvg` | Inline SVG for the card icon (only needed for new agents). |
|
|
89
|
+
| `rank` | Sort order on the launcher; lower comes first. |
|
|
90
|
+
| `enabled` | `false` hides the card from the launcher and palette. |
|
|
91
|
+
| `requireAvailable` | `false` skips the `which` check (use for shell aliases that aren't on PATH but expand at runtime). |
|
|
92
|
+
| `promptArgs` | How to splice the launcher's optional prompt into the command. `[]` appends it as a positional argument (`<command> 'PROMPT'`); `["-i"]` or `["--prompt"]` puts it after a flag. Set to `null` to opt out — the agent button is dimmed when the prompt textarea is non-empty so a typed prompt is never silently dropped. Defaults vary per agent. |
|
|
38
93
|
|
|
39
94
|
## Default settings
|
|
40
95
|
|
|
@@ -46,6 +101,10 @@ override versions and Jupyter configuration in the usual ways.
|
|
|
46
101
|
they do not show up in the right sidebar by default.
|
|
47
102
|
- `dockPanelPadding` is off, so the main dock area is flush with the
|
|
48
103
|
surrounding chrome.
|
|
104
|
+
- The terminal `fontFamily` defaults to `MesloLGS NF, ui-monospace, monospace`
|
|
105
|
+
so Powerline / Nerd Font glyphs (used by Oh My Zsh themes such as
|
|
106
|
+
`powerlevel10k`) render correctly when the font is installed; the
|
|
107
|
+
`ui-monospace, monospace` fallback keeps things readable when it is not.
|
|
49
108
|
|
|
50
109
|
The JupyterLab frontend defaults are shipped as `labconfig/*.d/00-xtralab.json`
|
|
51
110
|
fragments so downstream meta-packages can add their own Lab configuration
|
|
@@ -57,6 +116,31 @@ fragments without replacing `xtralab`'s files.
|
|
|
57
116
|
pip install xtralab
|
|
58
117
|
```
|
|
59
118
|
|
|
119
|
+
## Run as a desktop app
|
|
120
|
+
|
|
121
|
+
`xtralab` ships a tiny launcher that opens JupyterLab in a dedicated Chrome
|
|
122
|
+
window (via Chrome's `--app` mode), with an isolated user-data directory so it
|
|
123
|
+
does not share cookies, extensions, or history with your normal browser.
|
|
124
|
+
|
|
125
|
+
Requires Chrome or Chromium installed. macOS only for now; other platforms will be added later.
|
|
126
|
+
|
|
127
|
+
The simplest way, no install needed:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
uvx xtralab
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Or, after `pip install xtralab` into an environment:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
xtralab
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The launcher starts a local Jupyter server on a random port with a fresh
|
|
140
|
+
token, waits for it to come up, opens the app window, and shuts the server
|
|
141
|
+
down when you close the window. `Ctrl-C` in the launching terminal also
|
|
142
|
+
shuts everything down cleanly.
|
|
143
|
+
|
|
60
144
|
## Development
|
|
61
145
|
|
|
62
146
|
```bash
|
|
@@ -4,6 +4,7 @@ import { MimeData, PromiseDelegate } from '@lumino/coreutils';
|
|
|
4
4
|
import { Drag } from '@lumino/dragdrop';
|
|
5
5
|
import { FileTree, useFileTree } from '@pierre/trees/react';
|
|
6
6
|
import { ROOT_LOAD_KEY, listDirectory, toServerPath } from './contents';
|
|
7
|
+
import { buildIgnoredEntries, loadGitignoreMatcher } from './gitignore';
|
|
7
8
|
import { FILE_BROWSER_ICONS } from './icons';
|
|
8
9
|
/**
|
|
9
10
|
* MIME types used by the dock panel and other JupyterLab drop targets.
|
|
@@ -41,11 +42,54 @@ export function FileBrowserComponent(props) {
|
|
|
41
42
|
paths: [],
|
|
42
43
|
initialExpansion: 'closed',
|
|
43
44
|
search: true,
|
|
44
|
-
icons: FILE_BROWSER_ICONS
|
|
45
|
+
icons: FILE_BROWSER_ICONS,
|
|
46
|
+
itemHeight: 24
|
|
45
47
|
});
|
|
46
48
|
React.useEffect(() => {
|
|
47
49
|
const knownDirs = new Map();
|
|
50
|
+
// Mirror of the canonical paths currently loaded into the tree. Kept in
|
|
51
|
+
// sync with `model.resetPaths`/`model.batch`/`model.add` so we can
|
|
52
|
+
// re-test every loaded path against the gitignore matcher whenever
|
|
53
|
+
// either side changes — the model itself does not expose a
|
|
54
|
+
// path-iteration API.
|
|
55
|
+
const loadedPaths = new Set();
|
|
56
|
+
let gitignoreMatcher = null;
|
|
48
57
|
let cancelled = false;
|
|
58
|
+
/**
|
|
59
|
+
* Recompute the `ignored` git status entries from the current
|
|
60
|
+
* gitignore matcher and the set of loaded paths, and push them into
|
|
61
|
+
* the tree. Safe to call at any time: when the matcher is `null`
|
|
62
|
+
* (no `.gitignore` or it failed to load) we send an empty list, which
|
|
63
|
+
* also clears any statuses that may have been applied previously.
|
|
64
|
+
*/
|
|
65
|
+
const syncGitStatus = () => {
|
|
66
|
+
if (gitignoreMatcher === null) {
|
|
67
|
+
model.setGitStatus([]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const entries = buildIgnoredEntries(gitignoreMatcher, loadedPaths);
|
|
71
|
+
model.setGitStatus(entries);
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Reload the workspace `.gitignore`, then re-apply the resulting
|
|
75
|
+
* ignored statuses. Called on initial mount and whenever the user
|
|
76
|
+
* triggers a refresh — the file may have been edited or created in
|
|
77
|
+
* between.
|
|
78
|
+
*/
|
|
79
|
+
const refreshGitignoreMatcher = async () => {
|
|
80
|
+
let next = null;
|
|
81
|
+
try {
|
|
82
|
+
next = await loadGitignoreMatcher(contentsManager);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error('xtralab: failed to load .gitignore', err);
|
|
86
|
+
}
|
|
87
|
+
if (cancelled) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
gitignoreMatcher = next;
|
|
91
|
+
syncGitStatus();
|
|
92
|
+
};
|
|
49
93
|
const fetchDirectory = async (canonicalPath) => {
|
|
50
94
|
const current = knownDirs.get(canonicalPath);
|
|
51
95
|
if (current === 'loading' || current === 'loaded') {
|
|
@@ -60,6 +104,10 @@ export function FileBrowserComponent(props) {
|
|
|
60
104
|
}
|
|
61
105
|
if (canonicalPath === ROOT_LOAD_KEY) {
|
|
62
106
|
model.resetPaths(paths);
|
|
107
|
+
loadedPaths.clear();
|
|
108
|
+
for (const path of paths) {
|
|
109
|
+
loadedPaths.add(path);
|
|
110
|
+
}
|
|
63
111
|
}
|
|
64
112
|
else {
|
|
65
113
|
// Filter out paths already in the model. The path-store throws
|
|
@@ -72,6 +120,14 @@ export function FileBrowserComponent(props) {
|
|
|
72
120
|
if (operations.length > 0) {
|
|
73
121
|
model.batch(operations);
|
|
74
122
|
}
|
|
123
|
+
// Mirror every directory child into `loadedPaths` regardless of
|
|
124
|
+
// whether it was just added or skipped above — paths skipped by
|
|
125
|
+
// the filter are already in the model from a prior add and so
|
|
126
|
+
// belong in the mirror too. `Set.add` is idempotent, so the
|
|
127
|
+
// duplicate adds are a no-op.
|
|
128
|
+
for (const path of paths) {
|
|
129
|
+
loadedPaths.add(path);
|
|
130
|
+
}
|
|
75
131
|
}
|
|
76
132
|
for (const subdir of subdirectories) {
|
|
77
133
|
if (!knownDirs.has(subdir)) {
|
|
@@ -79,6 +135,7 @@ export function FileBrowserComponent(props) {
|
|
|
79
135
|
}
|
|
80
136
|
}
|
|
81
137
|
knownDirs.set(canonicalPath, 'loaded');
|
|
138
|
+
syncGitStatus();
|
|
82
139
|
}
|
|
83
140
|
catch (err) {
|
|
84
141
|
console.error(`xtralab: failed to load directory "${canonicalPath}"`, err);
|
|
@@ -165,12 +222,22 @@ export function FileBrowserComponent(props) {
|
|
|
165
222
|
// Reset the tree contents and restore the load-state map so future
|
|
166
223
|
// expansions know which directories still need fetching.
|
|
167
224
|
model.resetPaths(allPaths);
|
|
225
|
+
loadedPaths.clear();
|
|
226
|
+
for (const path of allPaths) {
|
|
227
|
+
loadedPaths.add(path);
|
|
228
|
+
}
|
|
168
229
|
knownDirs.set(ROOT_LOAD_KEY, 'loaded');
|
|
169
230
|
subdirsByParent.forEach(subdirs => {
|
|
170
231
|
for (const subdir of subdirs) {
|
|
171
232
|
knownDirs.set(subdir, expandedPaths.has(subdir) ? 'loaded' : 'unloaded');
|
|
172
233
|
}
|
|
173
234
|
});
|
|
235
|
+
// Refresh the `.gitignore` matcher in case the file was edited
|
|
236
|
+
// since the last load, then re-apply the resulting statuses to the
|
|
237
|
+
// newly-loaded paths. The reload runs in parallel with the rest of
|
|
238
|
+
// the refresh — `refreshGitignoreMatcher` calls `syncGitStatus`
|
|
239
|
+
// itself when it completes.
|
|
240
|
+
void refreshGitignoreMatcher();
|
|
174
241
|
// Re-expand the directories that were expanded before the refresh.
|
|
175
242
|
// We have to do this after `resetPaths` because the reset starts
|
|
176
243
|
// every directory in its initial collapsed state.
|
|
@@ -196,12 +263,14 @@ export function FileBrowserComponent(props) {
|
|
|
196
263
|
return;
|
|
197
264
|
}
|
|
198
265
|
}
|
|
266
|
+
loadedPaths.add(canonicalPath);
|
|
199
267
|
if (canonicalPath.endsWith('/') && !knownDirs.has(canonicalPath)) {
|
|
200
268
|
// The new directory has no children yet, so mark it as already
|
|
201
269
|
// loaded — there's nothing to fetch and we don't want a stale
|
|
202
270
|
// "unloaded" entry to trigger a fetch on the next expand.
|
|
203
271
|
knownDirs.set(canonicalPath, 'loaded');
|
|
204
272
|
}
|
|
273
|
+
syncGitStatus();
|
|
205
274
|
const parent = parentOf(canonicalPath);
|
|
206
275
|
if (parent === ROOT_LOAD_KEY) {
|
|
207
276
|
return;
|
|
@@ -217,6 +286,7 @@ export function FileBrowserComponent(props) {
|
|
|
217
286
|
};
|
|
218
287
|
knownDirs.set(ROOT_LOAD_KEY, 'unloaded');
|
|
219
288
|
void fetchDirectory(ROOT_LOAD_KEY);
|
|
289
|
+
void refreshGitignoreMatcher();
|
|
220
290
|
const unsubscribe = model.subscribe(() => {
|
|
221
291
|
knownDirs.forEach((state, canonicalPath) => {
|
|
222
292
|
if (state !== 'unloaded') {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Ignore } from 'ignore';
|
|
2
|
+
import { Contents } from '@jupyterlab/services';
|
|
3
|
+
import type { GitStatusEntry } from '@pierre/trees';
|
|
4
|
+
/**
|
|
5
|
+
* Load the workspace `.gitignore` and return an `ignore` matcher built from
|
|
6
|
+
* its contents. Returns `null` when the file is missing, empty, or fails to
|
|
7
|
+
* load — callers should treat any of those as "no ignored entries".
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadGitignoreMatcher(contentsManager: Contents.IManager): Promise<Ignore | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Build `GitStatusEntry`s for the canonical paths that match the given
|
|
12
|
+
* gitignore matcher.
|
|
13
|
+
*
|
|
14
|
+
* `@pierre/trees` propagates the `ignored` style from a directory entry
|
|
15
|
+
* down to every descendant via its internal `ignoredDirectoryPaths` set, so
|
|
16
|
+
* we only emit one entry per ignored subtree root: emitting descendants
|
|
17
|
+
* would also pollute the library's `directoriesWithChanges` set, causing
|
|
18
|
+
* parent directories of nested ignored items to render a "contains git
|
|
19
|
+
* status items" dot.
|
|
20
|
+
*
|
|
21
|
+
* Paths are sorted lexicographically so a directory always precedes its
|
|
22
|
+
* descendants, allowing a simple ancestor-suppression check via
|
|
23
|
+
* `path.startsWith(dir)`. The trailing slash on canonical directory paths
|
|
24
|
+
* (required by `@pierre/trees`, also recognized by `ignore` as the
|
|
25
|
+
* "directory" indicator) keeps the prefix check from accidentally matching
|
|
26
|
+
* sibling paths that happen to share a name prefix (e.g. `node_modules_old`
|
|
27
|
+
* vs `node_modules/`).
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildIgnoredEntries(matcher: Ignore, canonicalPaths: Iterable<string>): GitStatusEntry[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import ignore from 'ignore';
|
|
2
|
+
/**
|
|
3
|
+
* The path of the workspace `.gitignore` file. Loaded relative to the
|
|
4
|
+
* Jupyter contents root, which matches the directory the gitignore patterns
|
|
5
|
+
* are evaluated against — patterns and tested paths share the same base.
|
|
6
|
+
*/
|
|
7
|
+
const GITIGNORE_PATH = '.gitignore';
|
|
8
|
+
/**
|
|
9
|
+
* Load the workspace `.gitignore` and return an `ignore` matcher built from
|
|
10
|
+
* its contents. Returns `null` when the file is missing, empty, or fails to
|
|
11
|
+
* load — callers should treat any of those as "no ignored entries".
|
|
12
|
+
*/
|
|
13
|
+
export async function loadGitignoreMatcher(contentsManager) {
|
|
14
|
+
let model;
|
|
15
|
+
try {
|
|
16
|
+
model = await contentsManager.get(GITIGNORE_PATH, {
|
|
17
|
+
content: true,
|
|
18
|
+
format: 'text',
|
|
19
|
+
type: 'file'
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (_a) {
|
|
23
|
+
// The most common failure here is `.gitignore` simply not existing.
|
|
24
|
+
// Other transient errors (permission denied, server unreachable) are
|
|
25
|
+
// also treated as "no matcher" so the file browser still works.
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (model.type !== 'file' || typeof model.content !== 'string') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (model.content.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return ignore().add(model.content);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build `GitStatusEntry`s for the canonical paths that match the given
|
|
38
|
+
* gitignore matcher.
|
|
39
|
+
*
|
|
40
|
+
* `@pierre/trees` propagates the `ignored` style from a directory entry
|
|
41
|
+
* down to every descendant via its internal `ignoredDirectoryPaths` set, so
|
|
42
|
+
* we only emit one entry per ignored subtree root: emitting descendants
|
|
43
|
+
* would also pollute the library's `directoriesWithChanges` set, causing
|
|
44
|
+
* parent directories of nested ignored items to render a "contains git
|
|
45
|
+
* status items" dot.
|
|
46
|
+
*
|
|
47
|
+
* Paths are sorted lexicographically so a directory always precedes its
|
|
48
|
+
* descendants, allowing a simple ancestor-suppression check via
|
|
49
|
+
* `path.startsWith(dir)`. The trailing slash on canonical directory paths
|
|
50
|
+
* (required by `@pierre/trees`, also recognized by `ignore` as the
|
|
51
|
+
* "directory" indicator) keeps the prefix check from accidentally matching
|
|
52
|
+
* sibling paths that happen to share a name prefix (e.g. `node_modules_old`
|
|
53
|
+
* vs `node_modules/`).
|
|
54
|
+
*/
|
|
55
|
+
export function buildIgnoredEntries(matcher, canonicalPaths) {
|
|
56
|
+
const sorted = Array.from(canonicalPaths).sort();
|
|
57
|
+
const emittedIgnoredDirs = [];
|
|
58
|
+
const entries = [];
|
|
59
|
+
for (const path of sorted) {
|
|
60
|
+
if (path.length === 0) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (emittedIgnoredDirs.some(dir => path.startsWith(dir))) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (matcher.ignores(path)) {
|
|
67
|
+
entries.push({ path, status: 'ignored' });
|
|
68
|
+
if (path.endsWith('/')) {
|
|
69
|
+
emittedIgnoredDirs.push(path);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return entries;
|
|
74
|
+
}
|
package/lib/git/api.d.ts
CHANGED
|
@@ -28,6 +28,17 @@ export declare function reset(repoPath: string, filename: string): Promise<void>
|
|
|
28
28
|
export declare function resetAll(repoPath: string): Promise<void>;
|
|
29
29
|
/** Restore a single file to the version in the index (i.e. discard worktree changes). */
|
|
30
30
|
export declare function checkout(repoPath: string, filename: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Expand the porcelain `files` array into one entry per logical change.
|
|
33
|
+
* A single porcelain row may map to two `IFileChange` rows (a file that's
|
|
34
|
+
* both staged and modified again in the worktree); callers that just want
|
|
35
|
+
* a flat list of all changes — without any staged-vs-unstaged grouping —
|
|
36
|
+
* can use this helper.
|
|
37
|
+
*
|
|
38
|
+
* Provided as a manual concat rather than `Array.prototype.flatMap` so we
|
|
39
|
+
* don't pin our `tsconfig.lib` to ES2019.
|
|
40
|
+
*/
|
|
41
|
+
export declare function expandStatusFiles(files: IGitStatusFile[]): IFileChange[];
|
|
31
42
|
/**
|
|
32
43
|
* Translate one entry in the porcelain `files` array into the panel's
|
|
33
44
|
* preferred shape. A single porcelain entry may represent up to two changes
|
package/lib/git/api.js
CHANGED
|
@@ -103,6 +103,25 @@ export async function checkout(repoPath, filename) {
|
|
|
103
103
|
filename
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Expand the porcelain `files` array into one entry per logical change.
|
|
108
|
+
* A single porcelain row may map to two `IFileChange` rows (a file that's
|
|
109
|
+
* both staged and modified again in the worktree); callers that just want
|
|
110
|
+
* a flat list of all changes — without any staged-vs-unstaged grouping —
|
|
111
|
+
* can use this helper.
|
|
112
|
+
*
|
|
113
|
+
* Provided as a manual concat rather than `Array.prototype.flatMap` so we
|
|
114
|
+
* don't pin our `tsconfig.lib` to ES2019.
|
|
115
|
+
*/
|
|
116
|
+
export function expandStatusFiles(files) {
|
|
117
|
+
const result = [];
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
for (const change of porcelainToFileChanges(file)) {
|
|
120
|
+
result.push(change);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
106
125
|
/**
|
|
107
126
|
* Translate one entry in the porcelain `files` array into the panel's
|
|
108
127
|
* preferred shape. A single porcelain entry may represent up to two changes
|
package/lib/git/commands.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export declare namespace CommandArguments {
|
|
|
24
24
|
interface IOpenDiff {
|
|
25
25
|
repoPath: string;
|
|
26
26
|
change: IFileChange;
|
|
27
|
+
pin?: boolean;
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
export interface IRegisterGitCommandsOptions {
|
|
@@ -58,7 +59,8 @@ export interface IRegisterGitCommandsOptions {
|
|
|
58
59
|
* Look up an already-open diff widget for a given file change. Returns
|
|
59
60
|
* `undefined` when none is open.
|
|
60
61
|
*/
|
|
61
|
-
findDiff(change: IFileChange): ReturnType<typeof createGitDiffWidget> | undefined;
|
|
62
|
+
findDiff(change: IFileChange, pin?: boolean): ReturnType<typeof createGitDiffWidget> | undefined;
|
|
63
|
+
onPinned(widget: ReturnType<typeof createGitDiffWidget>): void;
|
|
62
64
|
}
|
|
63
65
|
/**
|
|
64
66
|
* Register the git commands on the application command registry. Returns
|
package/lib/git/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { refreshIcon } from '@jupyterlab/ui-components';
|
|
2
|
-
import { createGitDiffWidget, diffWidgetId } from './diffWidget';
|
|
2
|
+
import { createGitDiffWidget, diffWidgetId, updateGitDiffWidget } from './diffWidget';
|
|
3
3
|
/**
|
|
4
4
|
* Command IDs exposed by the git plugin. Both are registered in
|
|
5
5
|
* {@link registerGitCommands} so other extensions, the command palette and
|
|
@@ -16,7 +16,7 @@ export var CommandIDs;
|
|
|
16
16
|
* (the commands themselves stay registered for the lifetime of the plugin).
|
|
17
17
|
*/
|
|
18
18
|
export function registerGitCommands(options) {
|
|
19
|
-
const { app, panel, themeManager, commandPalette, contentsManager, rendermime, onChanged, trackDiff, findDiff } = options;
|
|
19
|
+
const { app, panel, themeManager, commandPalette, contentsManager, rendermime, onChanged, trackDiff, findDiff, onPinned } = options;
|
|
20
20
|
const { commands } = app;
|
|
21
21
|
commands.addCommand(CommandIDs.refresh, {
|
|
22
22
|
label: 'Refresh Git Changes',
|
|
@@ -34,8 +34,12 @@ export function registerGitCommands(options) {
|
|
|
34
34
|
if ((typed === null || typed === void 0 ? void 0 : typed.change) === undefined) {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
const
|
|
37
|
+
const pin = typed.pin === true;
|
|
38
|
+
const existing = findDiff(typed.change, pin);
|
|
38
39
|
if (existing !== undefined && !existing.isDisposed) {
|
|
40
|
+
if (!pin) {
|
|
41
|
+
updateGitDiffWidget(existing, typed.change);
|
|
42
|
+
}
|
|
39
43
|
app.shell.activateById(existing.id);
|
|
40
44
|
return;
|
|
41
45
|
}
|
|
@@ -45,7 +49,9 @@ export function registerGitCommands(options) {
|
|
|
45
49
|
themeManager,
|
|
46
50
|
contentsManager,
|
|
47
51
|
rendermime,
|
|
48
|
-
onChanged
|
|
52
|
+
onChanged,
|
|
53
|
+
pinned: pin,
|
|
54
|
+
onPinned
|
|
49
55
|
});
|
|
50
56
|
await trackDiff(widget);
|
|
51
57
|
app.shell.add(widget, 'main', { mode: 'tab-after' });
|
package/lib/git/diffWidget.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { GitReference, IFileChange } from './tokens';
|
|
|
10
10
|
* style the embedded `@pierre/diffs` viewer hang off this class.
|
|
11
11
|
*/
|
|
12
12
|
export declare const DIFF_WIDGET_CSS_CLASS = "jp-xtralab-DiffWidget";
|
|
13
|
+
export declare const PREVIEW_DIFF_WIDGET_ID = "xtralab:diff:preview";
|
|
14
|
+
export declare function pinnedDiffWidgetId(change: IFileChange): string;
|
|
13
15
|
/**
|
|
14
16
|
* View modes available for `.ipynb` diffs. `notebook` is the cell-by-cell
|
|
15
17
|
* rendered view; `json` falls back to the raw nbformat JSON file diff so
|
|
@@ -58,6 +60,8 @@ export interface IGitDiffWidgetOptions {
|
|
|
58
60
|
* would just be visual noise.
|
|
59
61
|
*/
|
|
60
62
|
onEmpty?: () => void;
|
|
63
|
+
onPinned?: (widget: MainAreaWidget<DiffContentWidget>) => void;
|
|
64
|
+
pinned?: boolean;
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
67
|
* Build a `MainAreaWidget`-hosted diff viewer for a single file change.
|
|
@@ -67,6 +71,7 @@ export interface IGitDiffWidgetOptions {
|
|
|
67
71
|
* read-only main-area widgets in the JupyterLab ecosystem.
|
|
68
72
|
*/
|
|
69
73
|
export declare function createGitDiffWidget(options: IGitDiffWidgetOptions): MainAreaWidget<DiffContentWidget>;
|
|
74
|
+
export declare function updateGitDiffWidget(widget: MainAreaWidget<DiffContentWidget>, change: IFileChange): void;
|
|
70
75
|
/**
|
|
71
76
|
* The actual React-rendered region of the diff viewer. Held in its own
|
|
72
77
|
* `ReactWidget` so the surrounding `MainAreaWidget` can manage chrome
|
|
@@ -98,11 +103,17 @@ export declare class DiffContentWidget extends ReactWidget {
|
|
|
98
103
|
setHasNotebookView(value: boolean): void;
|
|
99
104
|
/** Emits whenever {@link hasNotebookView} changes. */
|
|
100
105
|
get hasNotebookViewChanged(): ISignal<this, boolean>;
|
|
106
|
+
get pinned(): boolean;
|
|
107
|
+
pin(): void;
|
|
108
|
+
get pinnedChanged(): ISignal<this, boolean>;
|
|
109
|
+
setChange(change: IFileChange): void;
|
|
101
110
|
protected render(): React.ReactElement;
|
|
102
111
|
private _options;
|
|
103
112
|
private _notebookViewMode;
|
|
104
113
|
private _notebookViewModeChanged;
|
|
105
114
|
private _hasNotebookView;
|
|
106
115
|
private _hasNotebookViewChanged;
|
|
116
|
+
private _pinned;
|
|
117
|
+
private _pinnedChanged;
|
|
107
118
|
}
|
|
108
119
|
export {};
|