xtralab 0.2.0 → 0.4.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 +126 -125
- package/lib/editorBreadcrumbs/index.d.ts +11 -0
- package/lib/editorBreadcrumbs/index.js +41 -0
- package/lib/editorBreadcrumbs/widget.d.ts +34 -0
- package/lib/editorBreadcrumbs/widget.js +131 -0
- package/lib/editorIndent/index.d.ts +17 -0
- package/lib/editorIndent/index.js +142 -0
- package/lib/fileBrowser/commands.d.ts +5 -2
- package/lib/fileBrowser/commands.js +42 -3
- package/lib/fileBrowser/fileBrowser.js +394 -11
- package/lib/fileBrowser/gitStatus.d.ts +13 -0
- package/lib/fileBrowser/gitStatus.js +60 -0
- package/lib/fileBrowser/icons.d.ts +4 -4
- package/lib/fileBrowser/icons.js +4 -4
- package/lib/fileBrowser/widget.d.ts +50 -2
- package/lib/fileBrowser/widget.js +26 -2
- package/lib/git/icons.d.ts +1 -1
- package/lib/git/icons.js +1 -1
- package/lib/git/panelComponent.js +28 -11
- package/lib/index.js +4 -0
- package/lib/launcher/dashboard.js +78 -16
- package/package.json +22 -2
- package/src/editorBreadcrumbs/index.ts +70 -0
- package/src/editorBreadcrumbs/widget.ts +158 -0
- package/src/editorIndent/index.ts +177 -0
- package/src/fileBrowser/commands.ts +45 -3
- package/src/fileBrowser/fileBrowser.tsx +428 -12
- package/src/fileBrowser/gitStatus.ts +66 -0
- package/src/fileBrowser/icons.ts +4 -4
- package/src/fileBrowser/widget.tsx +80 -3
- package/src/git/icons.ts +1 -1
- package/src/git/panelComponent.tsx +29 -15
- package/src/index.ts +4 -0
- package/src/launcher/dashboard.tsx +135 -16
- package/style/editorBreadcrumbs.css +86 -0
- package/style/index.css +2 -0
- package/style/index.js +1 -0
- package/style/launcher.css +0 -5
- package/style/tabs.css +66 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { expandStatusFiles, status } from '../git/api';
|
|
2
|
+
/**
|
|
3
|
+
* Translate a `jupyterlab_git` porcelain status into the closest
|
|
4
|
+
* `@pierre/trees` `GitStatus` enum value. The tree library only supports the
|
|
5
|
+
* six common statuses (added, deleted, ignored, modified, renamed, untracked)
|
|
6
|
+
* so the rare ones (unmerged, typechange, unknown) collapse onto `modified`
|
|
7
|
+
* — they all denote a pending change the user should see decorated.
|
|
8
|
+
*/
|
|
9
|
+
function toTreeStatus(value) {
|
|
10
|
+
switch (value) {
|
|
11
|
+
case 'added':
|
|
12
|
+
case 'deleted':
|
|
13
|
+
case 'modified':
|
|
14
|
+
case 'renamed':
|
|
15
|
+
case 'untracked':
|
|
16
|
+
return value;
|
|
17
|
+
case 'unmerged':
|
|
18
|
+
case 'typechange':
|
|
19
|
+
case 'unknown':
|
|
20
|
+
default:
|
|
21
|
+
return 'modified';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Fetch the porcelain status for `repoPath` and convert it into a flat list
|
|
26
|
+
* of `GitStatusEntry` values consumable by `FileTree.setGitStatus`. Returns
|
|
27
|
+
* an empty array when the path is not in a git repo, the request fails, or
|
|
28
|
+
* the server reports a non-zero exit — the file browser must still work
|
|
29
|
+
* without git, so any error here is a no-op rather than a thrown exception.
|
|
30
|
+
*
|
|
31
|
+
* Files that have both a staged and an unstaged change are collapsed into a
|
|
32
|
+
* single entry: the unstaged status wins because that mirrors what is
|
|
33
|
+
* actually different on disk and matches VS Code's tree decoration behavior.
|
|
34
|
+
*/
|
|
35
|
+
export async function loadGitStatusEntries(repoPath) {
|
|
36
|
+
try {
|
|
37
|
+
const result = await status(repoPath);
|
|
38
|
+
if (result.code !== 0) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const changes = expandStatusFiles(result.files);
|
|
42
|
+
// Collapse staged + unstaged into one decoration per file. Prefer the
|
|
43
|
+
// unstaged group when both exist; otherwise keep whichever group is
|
|
44
|
+
// present.
|
|
45
|
+
const byPath = new Map();
|
|
46
|
+
for (const change of changes) {
|
|
47
|
+
if (change.group === 'unstaged' || !byPath.has(change.path)) {
|
|
48
|
+
byPath.set(change.path, change.status);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const entries = [];
|
|
52
|
+
byPath.forEach((value, path) => {
|
|
53
|
+
entries.push({ path, status: toTreeStatus(value) });
|
|
54
|
+
});
|
|
55
|
+
return entries;
|
|
56
|
+
}
|
|
57
|
+
catch (_a) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
2
|
import type { FileTreeIconConfig } from '@pierre/trees';
|
|
3
3
|
/**
|
|
4
|
-
* The glyph used as the
|
|
4
|
+
* The glyph used as the xtralab file browser's sidebar tab icon. Defined as a
|
|
5
5
|
* `LabIcon` (rather than the default `folderIcon` from
|
|
6
|
-
* `@jupyterlab/ui-components`) so the
|
|
6
|
+
* `@jupyterlab/ui-components`) so the xtralab browser is visually distinguishable
|
|
7
7
|
* from JupyterLab's built-in file browser, which also lives in the left
|
|
8
8
|
* sidebar.
|
|
9
9
|
*
|
|
@@ -14,7 +14,7 @@ import type { FileTreeIconConfig } from '@pierre/trees';
|
|
|
14
14
|
*/
|
|
15
15
|
export declare const xtralabFileBrowserIcon: LabIcon;
|
|
16
16
|
/**
|
|
17
|
-
* Icon configuration for the
|
|
17
|
+
* Icon configuration for the xtralab file browser. Keeps the default `complete`
|
|
18
18
|
* built-in icon set (the colored language icons) and layers a Jupyter notebook
|
|
19
19
|
* symbol on top so `.ipynb` files render with the JupyterLab notebook glyph.
|
|
20
20
|
*
|
|
@@ -24,7 +24,7 @@ export declare const xtralabFileBrowserIcon: LabIcon;
|
|
|
24
24
|
*/
|
|
25
25
|
export declare const FILE_BROWSER_ICONS: FileTreeIconConfig;
|
|
26
26
|
/**
|
|
27
|
-
* Return a JupyterLab `LabIcon` matching the icon the
|
|
27
|
+
* Return a JupyterLab `LabIcon` matching the icon the xtralab file tree would
|
|
28
28
|
* render for the given file path. Used by other plugins (e.g. the git diff
|
|
29
29
|
* viewer) that want their main-area widget tabs to display the same per-file
|
|
30
30
|
* glyph the tree shows in the sidebar.
|
package/lib/fileBrowser/icons.js
CHANGED
|
@@ -2,9 +2,9 @@ var _a;
|
|
|
2
2
|
import { LabIcon, fileIcon } from '@jupyterlab/ui-components';
|
|
3
3
|
import { createFileTreeIconResolver, getBuiltInFileIconColor, getBuiltInSpriteSheet } from '@pierre/trees';
|
|
4
4
|
/**
|
|
5
|
-
* The glyph used as the
|
|
5
|
+
* The glyph used as the xtralab file browser's sidebar tab icon. Defined as a
|
|
6
6
|
* `LabIcon` (rather than the default `folderIcon` from
|
|
7
|
-
* `@jupyterlab/ui-components`) so the
|
|
7
|
+
* `@jupyterlab/ui-components`) so the xtralab browser is visually distinguishable
|
|
8
8
|
* from JupyterLab's built-in file browser, which also lives in the left
|
|
9
9
|
* sidebar.
|
|
10
10
|
*
|
|
@@ -42,7 +42,7 @@ const JUPYTER_NOTEBOOK_SPRITE_SHEET = `<svg data-icon-sprite aria-hidden="true"
|
|
|
42
42
|
</symbol>
|
|
43
43
|
</svg>`;
|
|
44
44
|
/**
|
|
45
|
-
* Icon configuration for the
|
|
45
|
+
* Icon configuration for the xtralab file browser. Keeps the default `complete`
|
|
46
46
|
* built-in icon set (the colored language icons) and layers a Jupyter notebook
|
|
47
47
|
* symbol on top so `.ipynb` files render with the JupyterLab notebook glyph.
|
|
48
48
|
*
|
|
@@ -105,7 +105,7 @@ function extractSymbol(symbolId) {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* Return a JupyterLab `LabIcon` matching the icon the
|
|
108
|
+
* Return a JupyterLab `LabIcon` matching the icon the xtralab file tree would
|
|
109
109
|
* render for the given file path. Used by other plugins (e.g. the git diff
|
|
110
110
|
* viewer) that want their main-area widget tabs to display the same per-file
|
|
111
111
|
* glyph the tree shows in the sidebar.
|
|
@@ -5,7 +5,7 @@ import { ISignal } from '@lumino/signaling';
|
|
|
5
5
|
import { Widget } from '@lumino/widgets';
|
|
6
6
|
export declare const FILE_BROWSER_ID = "xtralab:file-browser";
|
|
7
7
|
/**
|
|
8
|
-
* The CSS class added to the
|
|
8
|
+
* The CSS class added to the xtralab file browser widget. The selectors that
|
|
9
9
|
* bind application context menu items hang off this class, so it must remain
|
|
10
10
|
* specific enough not to clash with other browsers (such as the default
|
|
11
11
|
* `jp-FileBrowser`).
|
|
@@ -17,7 +17,7 @@ export interface IXtralabFileBrowserOptions {
|
|
|
17
17
|
onOpenFile?: (serverPath: string) => void;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Public API of the
|
|
20
|
+
* Public API of the xtralab file browser used by command handlers and other
|
|
21
21
|
* collaborators. Kept narrow on purpose so the React component stays free to
|
|
22
22
|
* evolve the underlying tree integration.
|
|
23
23
|
*/
|
|
@@ -56,6 +56,28 @@ export interface IXtralabFileBrowser {
|
|
|
56
56
|
* sees the new item without a full refresh.
|
|
57
57
|
*/
|
|
58
58
|
readonly pathAdded: ISignal<IXtralabFileBrowser, string>;
|
|
59
|
+
/**
|
|
60
|
+
* Emits when an external caller asks the tree to scroll to and select
|
|
61
|
+
* the given canonical path. The React component listens, lazily loads
|
|
62
|
+
* any unloaded ancestor directories, expands them, and selects the
|
|
63
|
+
* target. Used by the editor breadcrumbs to jump back to a file or
|
|
64
|
+
* folder shown in the breadcrumb trail.
|
|
65
|
+
*/
|
|
66
|
+
readonly revealRequested: ISignal<IXtralabFileBrowser, string>;
|
|
67
|
+
/**
|
|
68
|
+
* Emits when an external caller asks the tree to return to the
|
|
69
|
+
* workspace root: clear any current selection and scroll back to the
|
|
70
|
+
* first row. Distinct from {@link revealRequested} because the
|
|
71
|
+
* workspace root has no tree row of its own — it cannot be reached
|
|
72
|
+
* by passing a path.
|
|
73
|
+
*/
|
|
74
|
+
readonly rootRequested: ISignal<IXtralabFileBrowser, void>;
|
|
75
|
+
/**
|
|
76
|
+
* Emits when an external caller asks the tree to collapse every
|
|
77
|
+
* expanded folder. The React component listens and walks the loaded
|
|
78
|
+
* directories, calling `.collapse()` on each expanded one.
|
|
79
|
+
*/
|
|
80
|
+
readonly collapseAllRequested: ISignal<IXtralabFileBrowser, void>;
|
|
59
81
|
/** Trigger a refresh of every loaded directory in the tree. */
|
|
60
82
|
refresh(): void;
|
|
61
83
|
/**
|
|
@@ -64,6 +86,23 @@ export interface IXtralabFileBrowser {
|
|
|
64
86
|
* convention: directories carry a trailing slash, files do not.
|
|
65
87
|
*/
|
|
66
88
|
notifyPathAdded(canonicalPath: string): void;
|
|
89
|
+
/**
|
|
90
|
+
* Ask the tree to reveal {@link canonicalPath}: load and expand any
|
|
91
|
+
* unloaded ancestor directories, then select and scroll the target
|
|
92
|
+
* into view. `canonicalPath` follows the `@pierre/trees` convention
|
|
93
|
+
* (directories carry a trailing slash, files do not). Must be a
|
|
94
|
+
* non-empty path — use {@link scrollToRoot} for the root gesture.
|
|
95
|
+
*/
|
|
96
|
+
reveal(canonicalPath: string): void;
|
|
97
|
+
/**
|
|
98
|
+
* Ask the tree to return to the workspace root: clear any current
|
|
99
|
+
* selection and scroll back to the top of the tree.
|
|
100
|
+
*/
|
|
101
|
+
scrollToRoot(): void;
|
|
102
|
+
/**
|
|
103
|
+
* Ask the tree to collapse every currently expanded folder.
|
|
104
|
+
*/
|
|
105
|
+
collapseAll(): void;
|
|
67
106
|
}
|
|
68
107
|
/**
|
|
69
108
|
* Lumino widget that hosts the React-based `@pierre/trees` file browser.
|
|
@@ -78,6 +117,9 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
|
|
|
78
117
|
get selectionChanged(): ISignal<this, readonly string[]>;
|
|
79
118
|
get refreshRequested(): ISignal<this, void>;
|
|
80
119
|
get pathAdded(): ISignal<this, string>;
|
|
120
|
+
get revealRequested(): ISignal<this, string>;
|
|
121
|
+
get rootRequested(): ISignal<this, void>;
|
|
122
|
+
get collapseAllRequested(): ISignal<this, void>;
|
|
81
123
|
/**
|
|
82
124
|
* Update the cached selection. Called from the React tree when the
|
|
83
125
|
* underlying `@pierre/trees` model emits a selection change.
|
|
@@ -85,6 +127,9 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
|
|
|
85
127
|
updateSelection(paths: readonly string[]): void;
|
|
86
128
|
refresh(): void;
|
|
87
129
|
notifyPathAdded(canonicalPath: string): void;
|
|
130
|
+
reveal(canonicalPath: string): void;
|
|
131
|
+
scrollToRoot(): void;
|
|
132
|
+
collapseAll(): void;
|
|
88
133
|
private _contentsManager;
|
|
89
134
|
private _docManager;
|
|
90
135
|
private _onOpenFile;
|
|
@@ -92,6 +137,9 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
|
|
|
92
137
|
private _selectionChanged;
|
|
93
138
|
private _refreshRequested;
|
|
94
139
|
private _pathAdded;
|
|
140
|
+
private _revealRequested;
|
|
141
|
+
private _rootRequested;
|
|
142
|
+
private _collapseAllRequested;
|
|
95
143
|
private _toolbar;
|
|
96
144
|
private _content;
|
|
97
145
|
}
|
|
@@ -6,7 +6,7 @@ import { FileBrowserComponent } from './fileBrowser';
|
|
|
6
6
|
import { xtralabFileBrowserIcon } from './icons';
|
|
7
7
|
export const FILE_BROWSER_ID = 'xtralab:file-browser';
|
|
8
8
|
/**
|
|
9
|
-
* The CSS class added to the
|
|
9
|
+
* The CSS class added to the xtralab file browser widget. The selectors that
|
|
10
10
|
* bind application context menu items hang off this class, so it must remain
|
|
11
11
|
* specific enough not to clash with other browsers (such as the default
|
|
12
12
|
* `jp-FileBrowser`).
|
|
@@ -26,13 +26,16 @@ export class XtralabFileBrowser extends Widget {
|
|
|
26
26
|
this._selectionChanged = new Signal(this);
|
|
27
27
|
this._refreshRequested = new Signal(this);
|
|
28
28
|
this._pathAdded = new Signal(this);
|
|
29
|
+
this._revealRequested = new Signal(this);
|
|
30
|
+
this._rootRequested = new Signal(this);
|
|
31
|
+
this._collapseAllRequested = new Signal(this);
|
|
29
32
|
const layout = (this.layout = new PanelLayout());
|
|
30
33
|
this._contentsManager = options.contentsManager;
|
|
31
34
|
this._docManager = options.docManager;
|
|
32
35
|
this._onOpenFile = options.onOpenFile;
|
|
33
36
|
this.id = FILE_BROWSER_ID;
|
|
34
37
|
this.title.icon = xtralabFileBrowserIcon;
|
|
35
|
-
this.title.caption = '
|
|
38
|
+
this.title.caption = 'xtralab File Browser';
|
|
36
39
|
this.addClass(FILE_BROWSER_CSS_CLASS);
|
|
37
40
|
this._toolbar = new Toolbar();
|
|
38
41
|
this._toolbar.addClass(TOOLBAR_CSS_CLASS);
|
|
@@ -65,6 +68,15 @@ export class XtralabFileBrowser extends Widget {
|
|
|
65
68
|
get pathAdded() {
|
|
66
69
|
return this._pathAdded;
|
|
67
70
|
}
|
|
71
|
+
get revealRequested() {
|
|
72
|
+
return this._revealRequested;
|
|
73
|
+
}
|
|
74
|
+
get rootRequested() {
|
|
75
|
+
return this._rootRequested;
|
|
76
|
+
}
|
|
77
|
+
get collapseAllRequested() {
|
|
78
|
+
return this._collapseAllRequested;
|
|
79
|
+
}
|
|
68
80
|
/**
|
|
69
81
|
* Update the cached selection. Called from the React tree when the
|
|
70
82
|
* underlying `@pierre/trees` model emits a selection change.
|
|
@@ -79,6 +91,18 @@ export class XtralabFileBrowser extends Widget {
|
|
|
79
91
|
notifyPathAdded(canonicalPath) {
|
|
80
92
|
this._pathAdded.emit(canonicalPath);
|
|
81
93
|
}
|
|
94
|
+
reveal(canonicalPath) {
|
|
95
|
+
if (canonicalPath.length === 0) {
|
|
96
|
+
throw new Error('XtralabFileBrowser.reveal requires a non-empty canonical path; use scrollToRoot for the workspace root.');
|
|
97
|
+
}
|
|
98
|
+
this._revealRequested.emit(canonicalPath);
|
|
99
|
+
}
|
|
100
|
+
scrollToRoot() {
|
|
101
|
+
this._rootRequested.emit();
|
|
102
|
+
}
|
|
103
|
+
collapseAll() {
|
|
104
|
+
this._collapseAllRequested.emit();
|
|
105
|
+
}
|
|
82
106
|
}
|
|
83
107
|
/**
|
|
84
108
|
* The React-rendered region of the file browser. Lives inside the host
|
package/lib/git/icons.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { LabIcon } from '@jupyterlab/ui-components';
|
|
|
5
5
|
* recoloring and inverse-color sidebar painting automatically.
|
|
6
6
|
*
|
|
7
7
|
* Source: `git-branch` from SVG Repo (https://www.svgrepo.com/svg/377352/git-branch),
|
|
8
|
-
* part of the same Pixelarticons family as the
|
|
8
|
+
* part of the same Pixelarticons family as the xtralab file browser's `file-tree`
|
|
9
9
|
* icon, so the two sidebar tab glyphs read as a matched set. The original
|
|
10
10
|
* `fill="#000000"` is replaced with `fill="currentColor"` and the root element
|
|
11
11
|
* is given the `jp-icon3` class so the icon picks up JupyterLab's theme color
|
package/lib/git/icons.js
CHANGED
|
@@ -5,7 +5,7 @@ import { LabIcon } from '@jupyterlab/ui-components';
|
|
|
5
5
|
* recoloring and inverse-color sidebar painting automatically.
|
|
6
6
|
*
|
|
7
7
|
* Source: `git-branch` from SVG Repo (https://www.svgrepo.com/svg/377352/git-branch),
|
|
8
|
-
* part of the same Pixelarticons family as the
|
|
8
|
+
* part of the same Pixelarticons family as the xtralab file browser's `file-tree`
|
|
9
9
|
* icon, so the two sidebar tab glyphs read as a matched set. The original
|
|
10
10
|
* `fill="#000000"` is replaced with `fill="currentColor"` and the root element
|
|
11
11
|
* is given the `jp-icon3` class so the icon picks up JupyterLab's theme color
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { Poll } from '@lumino/polling';
|
|
2
3
|
import { add, addAll, checkout, porcelainToFileChanges, reset, resetAll, showTopLevel, status } from './api';
|
|
3
4
|
/**
|
|
4
5
|
* The polling interval for the panel. We refresh the status periodically so
|
|
@@ -6,6 +7,13 @@ import { add, addAll, checkout, porcelainToFileChanges, reset, resetAll, showTop
|
|
|
6
7
|
* visible without requiring a manual refresh.
|
|
7
8
|
*/
|
|
8
9
|
const STATUS_POLL_INTERVAL_MS = 5000;
|
|
10
|
+
/**
|
|
11
|
+
* Upper bound on the poll's exponential backoff when status requests fail
|
|
12
|
+
* repeatedly. Matches the default JupyterLab file browser's `max`: long
|
|
13
|
+
* enough that a server outage stops hammering the API, short enough that
|
|
14
|
+
* a transient failure heals on its own.
|
|
15
|
+
*/
|
|
16
|
+
const STATUS_POLL_MAX_MS = 300000;
|
|
9
17
|
/**
|
|
10
18
|
* Top-level React component rendered inside the {@link GitPanel} widget.
|
|
11
19
|
* Owns the polling + refresh wiring around the `/git/status` endpoint and
|
|
@@ -59,23 +67,32 @@ export function GitPanelComponent(props) {
|
|
|
59
67
|
});
|
|
60
68
|
}
|
|
61
69
|
}, [repoPath]);
|
|
62
|
-
// Initial load + polling.
|
|
63
|
-
// the
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}, [refresh]);
|
|
71
|
-
// External refresh triggers (toolbar button, plugin-level events).
|
|
70
|
+
// Initial load + polling. Driven by a Lumino `Poll` so the panel stops
|
|
71
|
+
// hitting the git endpoint while the JupyterLab tab is hidden, and so
|
|
72
|
+
// repeated failures back off exponentially instead of spinning at 5s.
|
|
73
|
+
// External refresh triggers (toolbar button, plugin-level events) call
|
|
74
|
+
// `poll.refresh()` to schedule an immediate tick instead of running
|
|
75
|
+
// `refresh()` directly — that keeps the panel from issuing two parallel
|
|
76
|
+
// status requests when an automatic tick happens to land at the same
|
|
77
|
+
// time as an external trigger.
|
|
72
78
|
React.useEffect(() => {
|
|
79
|
+
const poll = new Poll({
|
|
80
|
+
name: '@xtralab/git:panelStatus',
|
|
81
|
+
factory: () => refresh(),
|
|
82
|
+
frequency: {
|
|
83
|
+
interval: STATUS_POLL_INTERVAL_MS,
|
|
84
|
+
backoff: true,
|
|
85
|
+
max: STATUS_POLL_MAX_MS
|
|
86
|
+
},
|
|
87
|
+
standby: 'when-hidden'
|
|
88
|
+
});
|
|
73
89
|
const handler = () => {
|
|
74
|
-
void refresh();
|
|
90
|
+
void poll.refresh();
|
|
75
91
|
};
|
|
76
92
|
refreshSignal.connect(handler);
|
|
77
93
|
return () => {
|
|
78
94
|
refreshSignal.disconnect(handler);
|
|
95
|
+
poll.dispose();
|
|
79
96
|
};
|
|
80
97
|
}, [refresh, refreshSignal]);
|
|
81
98
|
const runAction = React.useCallback(async (change, action, run) => {
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import fileBrowserPlugin from './fileBrowser';
|
|
2
|
+
import editorBreadcrumbsPlugin from './editorBreadcrumbs';
|
|
3
|
+
import editorIndentPlugin from './editorIndent';
|
|
2
4
|
import gitPlugin from './git';
|
|
3
5
|
import launcherPlugin from './launcher';
|
|
4
6
|
import menusPlugin from './menus';
|
|
@@ -12,6 +14,8 @@ import statusBarPlugin from './statusBar';
|
|
|
12
14
|
* available end up running.
|
|
13
15
|
*/
|
|
14
16
|
const plugins = [
|
|
17
|
+
editorBreadcrumbsPlugin,
|
|
18
|
+
editorIndentPlugin,
|
|
15
19
|
fileBrowserPlugin,
|
|
16
20
|
gitPlugin,
|
|
17
21
|
launcherPlugin,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Poll } from '@lumino/polling';
|
|
3
|
+
import { LabIcon, ReactWidget, consoleIcon, notebookIcon, refreshIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
3
4
|
import { expandStatusFiles, status } from '../git/api';
|
|
4
5
|
import { CommandIDs as GitCommandIDs } from '../git/commands';
|
|
5
6
|
import { agentCommandId } from './commands';
|
|
@@ -24,6 +25,12 @@ export class LauncherDashboard extends ReactWidget {
|
|
|
24
25
|
* only opened intentionally, and the grid does not need to be live.
|
|
25
26
|
*/
|
|
26
27
|
const CHANGES_POLL_INTERVAL_MS = 8000;
|
|
28
|
+
/**
|
|
29
|
+
* Upper bound on the exponential backoff when the status request fails
|
|
30
|
+
* repeatedly. Matches the rest of the plugin's polls so behavior is
|
|
31
|
+
* consistent across the dashboard, the git panel, and the file tree.
|
|
32
|
+
*/
|
|
33
|
+
const CHANGES_POLL_MAX_MS = 300000;
|
|
27
34
|
function LauncherDashboardComponent(props) {
|
|
28
35
|
const { commands, agents, onAgentLaunch, repoPath, cwd } = props;
|
|
29
36
|
const [prompt, setPrompt] = React.useState('');
|
|
@@ -45,12 +52,23 @@ function LauncherDashboardComponent(props) {
|
|
|
45
52
|
});
|
|
46
53
|
}
|
|
47
54
|
}, [repoPath]);
|
|
55
|
+
// Driven by a Lumino `Poll` so the dashboard stops hitting the git
|
|
56
|
+
// endpoint while the JupyterLab tab is hidden, and so repeated
|
|
57
|
+
// failures back off exponentially instead of pegging the 8s cadence.
|
|
48
58
|
React.useEffect(() => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
const poll = new Poll({
|
|
60
|
+
name: '@xtralab/launcher:gitChanges',
|
|
61
|
+
factory: () => refresh(),
|
|
62
|
+
frequency: {
|
|
63
|
+
interval: CHANGES_POLL_INTERVAL_MS,
|
|
64
|
+
backoff: true,
|
|
65
|
+
max: CHANGES_POLL_MAX_MS
|
|
66
|
+
},
|
|
67
|
+
standby: 'when-hidden'
|
|
68
|
+
});
|
|
69
|
+
return () => {
|
|
70
|
+
poll.dispose();
|
|
71
|
+
};
|
|
54
72
|
}, [refresh]);
|
|
55
73
|
const launch = React.useCallback(async (agent) => {
|
|
56
74
|
const args = { cwd };
|
|
@@ -63,12 +81,40 @@ function LauncherDashboardComponent(props) {
|
|
|
63
81
|
onAgentLaunch(result);
|
|
64
82
|
}
|
|
65
83
|
}, [commands, cwd, prompt, onAgentLaunch]);
|
|
84
|
+
const launchTerminal = React.useCallback(async () => {
|
|
85
|
+
const result = (await commands.execute('terminal:create-new', {
|
|
86
|
+
cwd
|
|
87
|
+
}));
|
|
88
|
+
if (result) {
|
|
89
|
+
onAgentLaunch(result);
|
|
90
|
+
}
|
|
91
|
+
}, [commands, cwd, onAgentLaunch]);
|
|
92
|
+
const launchNotebook = React.useCallback(async () => {
|
|
93
|
+
// No `kernelName` — let the notebook extension fall back to the default
|
|
94
|
+
// kernel or prompt the user, matching the stock launcher's notebook
|
|
95
|
+
// tile behavior.
|
|
96
|
+
const result = (await commands.execute('notebook:create-new', {
|
|
97
|
+
cwd
|
|
98
|
+
}));
|
|
99
|
+
if (result) {
|
|
100
|
+
onAgentLaunch(result);
|
|
101
|
+
}
|
|
102
|
+
}, [commands, cwd, onAgentLaunch]);
|
|
103
|
+
const launchConsole = React.useCallback(async () => {
|
|
104
|
+
const result = (await commands.execute('console:create', {
|
|
105
|
+
cwd
|
|
106
|
+
}));
|
|
107
|
+
if (result) {
|
|
108
|
+
onAgentLaunch(result);
|
|
109
|
+
}
|
|
110
|
+
}, [commands, cwd, onAgentLaunch]);
|
|
66
111
|
const openDiff = React.useCallback((change) => {
|
|
67
112
|
const args = { repoPath, change };
|
|
68
113
|
void commands.execute(GitCommandIDs.openDiff, args);
|
|
69
114
|
}, [commands, repoPath]);
|
|
70
115
|
return (React.createElement("div", { className: "jp-xtralab-Launcher-body" },
|
|
71
116
|
React.createElement(AgentSection, { agents: agents, prompt: prompt, onPromptChange: setPrompt, onLaunch: launch }),
|
|
117
|
+
React.createElement(OpenSection, { onLaunchTerminal: launchTerminal, onLaunchNotebook: launchNotebook, onLaunchConsole: launchConsole }),
|
|
72
118
|
React.createElement(ChangesSection, { git: git, onOpen: openDiff, onRefresh: () => void refresh() })));
|
|
73
119
|
}
|
|
74
120
|
function ChangesSection(props) {
|
|
@@ -126,10 +172,9 @@ function AgentSection(props) {
|
|
|
126
172
|
const { agents, prompt, onPromptChange, onLaunch } = props;
|
|
127
173
|
const trimmed = prompt.trim();
|
|
128
174
|
const promptActive = trimmed.length > 0;
|
|
129
|
-
// The
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// textarea — picking by the same rule keeps the two behaviors in sync.
|
|
175
|
+
// The first prompt-capable agent is what Cmd/Ctrl+Enter fires; the hint
|
|
176
|
+
// text below the textarea names it so the keyboard shortcut target
|
|
177
|
+
// isn't a mystery.
|
|
133
178
|
const primaryAgent = agents.find(agent => agent.promptArgs !== undefined);
|
|
134
179
|
const hintId = 'jp-xtralab-Launcher-agent-hint';
|
|
135
180
|
return (React.createElement("section", { className: "jp-xtralab-Launcher-section" },
|
|
@@ -149,16 +194,33 @@ function AgentSection(props) {
|
|
|
149
194
|
React.createElement("div", { className: "jp-xtralab-Launcher-agents" }, agents.map(agent => {
|
|
150
195
|
const supportsPrompt = agent.promptArgs !== undefined;
|
|
151
196
|
const disabled = promptActive && !supportsPrompt;
|
|
152
|
-
const isPrimary = agent === primaryAgent;
|
|
153
197
|
const tooltip = disabled
|
|
154
198
|
? `${agent.label} doesn't accept an initial prompt — clear the prompt to launch.`
|
|
155
199
|
: agent.caption;
|
|
156
|
-
|
|
157
|
-
if (isPrimary) {
|
|
158
|
-
classes.push('jp-xtralab-Launcher-agent-primary');
|
|
159
|
-
}
|
|
160
|
-
return (React.createElement("button", { key: agent.id, type: "button", className: classes.join(' '), title: tooltip, "aria-label": agent.label, "aria-describedby": hintId, disabled: disabled, onClick: () => onLaunch(agent) },
|
|
200
|
+
return (React.createElement("button", { key: agent.id, type: "button", className: "jp-xtralab-Launcher-agent", title: tooltip, "aria-label": agent.label, "aria-describedby": hintId, disabled: disabled, onClick: () => onLaunch(agent) },
|
|
161
201
|
React.createElement(LabIcon.resolveReact, { icon: agent.icon, tag: "span", className: "jp-xtralab-Launcher-agent-icon" }),
|
|
162
202
|
React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, agent.label)));
|
|
163
203
|
}))));
|
|
164
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* The non-agent launch tiles — a plain terminal, a new notebook, and a
|
|
207
|
+
* new console. They sit in a separate section below the agent grid so
|
|
208
|
+
* they don't compete with the AI agents visually, and they're always
|
|
209
|
+
* available regardless of the agent prompt textarea (which doesn't
|
|
210
|
+
* apply to them).
|
|
211
|
+
*/
|
|
212
|
+
function OpenSection(props) {
|
|
213
|
+
const { onLaunchTerminal, onLaunchNotebook, onLaunchConsole } = props;
|
|
214
|
+
return (React.createElement("section", { className: "jp-xtralab-Launcher-section" },
|
|
215
|
+
React.createElement("h2", { className: "jp-xtralab-Launcher-section-title" }, "Open"),
|
|
216
|
+
React.createElement("div", { className: "jp-xtralab-Launcher-agents" },
|
|
217
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-Launcher-agent", title: "Open a new terminal.", "aria-label": "Open a new terminal", onClick: onLaunchTerminal },
|
|
218
|
+
React.createElement(LabIcon.resolveReact, { icon: terminalIcon, tag: "span", className: "jp-xtralab-Launcher-agent-icon" }),
|
|
219
|
+
React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, "Terminal")),
|
|
220
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-Launcher-agent", title: "Create a new notebook.", "aria-label": "Create a new notebook", onClick: onLaunchNotebook },
|
|
221
|
+
React.createElement(LabIcon.resolveReact, { icon: notebookIcon, tag: "span", className: "jp-xtralab-Launcher-agent-icon" }),
|
|
222
|
+
React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, "Notebook")),
|
|
223
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-Launcher-agent", title: "Create a new console.", "aria-label": "Create a new console", onClick: onLaunchConsole },
|
|
224
|
+
React.createElement(LabIcon.resolveReact, { icon: consoleIcon, tag: "span", className: "jp-xtralab-Launcher-agent-icon" }),
|
|
225
|
+
React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, "Console")))));
|
|
226
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtralab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser and a VS Code-style git changes panel, and applies a quieter default workspace configuration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -57,8 +57,12 @@
|
|
|
57
57
|
"watch:labextension": "jupyter-builder watch ."
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
+
"@codemirror/language": "^6.12.1",
|
|
61
|
+
"@codemirror/state": "^6.5.4",
|
|
62
|
+
"@codemirror/view": "^6.39.14",
|
|
60
63
|
"@jupyterlab/application": "^4.6.0-alpha.5",
|
|
61
64
|
"@jupyterlab/apputils": "^4.7.0-alpha.5",
|
|
65
|
+
"@jupyterlab/codemirror": "^4.6.0-alpha.5",
|
|
62
66
|
"@jupyterlab/coreutils": "^6.6.0-alpha.5",
|
|
63
67
|
"@jupyterlab/docmanager": "^4.6.0-alpha.5",
|
|
64
68
|
"@jupyterlab/docregistry": "^4.6.0-alpha.5",
|
|
@@ -78,10 +82,12 @@
|
|
|
78
82
|
"@lumino/disposable": "^2.0.0",
|
|
79
83
|
"@lumino/dragdrop": "^2.0.0",
|
|
80
84
|
"@lumino/messaging": "^2.0.0",
|
|
85
|
+
"@lumino/polling": "^2.1.5",
|
|
81
86
|
"@lumino/signaling": "^2.0.0",
|
|
82
87
|
"@lumino/widgets": "^2.0.0",
|
|
83
88
|
"@pierre/diffs": "^1.1.20",
|
|
84
89
|
"@pierre/trees": "1.0.0-beta.3",
|
|
90
|
+
"detect-indent": "^7.0.2",
|
|
85
91
|
"ignore": "^5.3.2",
|
|
86
92
|
"react": "^18.2.0",
|
|
87
93
|
"react-dom": "^18.2.0"
|
|
@@ -145,12 +151,26 @@
|
|
|
145
151
|
],
|
|
146
152
|
"parser": "@typescript-eslint/parser",
|
|
147
153
|
"parserOptions": {
|
|
148
|
-
"project":
|
|
154
|
+
"project": [
|
|
155
|
+
"tsconfig.json",
|
|
156
|
+
"desktop/tsconfig.json",
|
|
157
|
+
"desktop/tsconfig.forge.json"
|
|
158
|
+
],
|
|
149
159
|
"sourceType": "module"
|
|
150
160
|
},
|
|
151
161
|
"plugins": [
|
|
152
162
|
"@typescript-eslint"
|
|
153
163
|
],
|
|
164
|
+
"overrides": [
|
|
165
|
+
{
|
|
166
|
+
"files": [
|
|
167
|
+
"desktop/src/**/*.ts"
|
|
168
|
+
],
|
|
169
|
+
"rules": {
|
|
170
|
+
"@typescript-eslint/naming-convention": "off"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
],
|
|
154
174
|
"rules": {
|
|
155
175
|
"@typescript-eslint/naming-convention": [
|
|
156
176
|
"error",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
import type {
|
|
6
|
+
DocumentRegistry,
|
|
7
|
+
IDocumentWidget
|
|
8
|
+
} from '@jupyterlab/docregistry';
|
|
9
|
+
import type { CommandRegistry } from '@lumino/commands';
|
|
10
|
+
import { DisposableDelegate, type IDisposable } from '@lumino/disposable';
|
|
11
|
+
import type { Widget } from '@lumino/widgets';
|
|
12
|
+
|
|
13
|
+
import { EditorBreadcrumbs } from './widget';
|
|
14
|
+
|
|
15
|
+
const PLUGIN_ID = 'xtralab:editor-breadcrumbs';
|
|
16
|
+
const EDITOR_FACTORY = 'Editor';
|
|
17
|
+
const TOOLBAR_ITEM_NAME = 'xtralab-editor-breadcrumbs';
|
|
18
|
+
|
|
19
|
+
class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
|
|
20
|
+
IDocumentWidget<Widget, DocumentRegistry.IModel>,
|
|
21
|
+
DocumentRegistry.IModel
|
|
22
|
+
> {
|
|
23
|
+
constructor(commands: CommandRegistry) {
|
|
24
|
+
this._commands = commands;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
createNew(
|
|
28
|
+
widget: IDocumentWidget<Widget, DocumentRegistry.IModel>,
|
|
29
|
+
context: DocumentRegistry.IContext<DocumentRegistry.IModel>
|
|
30
|
+
): IDisposable | void {
|
|
31
|
+
const breadcrumbs = new EditorBreadcrumbs({
|
|
32
|
+
context,
|
|
33
|
+
commands: this._commands
|
|
34
|
+
});
|
|
35
|
+
const added = widget.toolbar.insertItem(0, TOOLBAR_ITEM_NAME, breadcrumbs);
|
|
36
|
+
|
|
37
|
+
if (!added) {
|
|
38
|
+
breadcrumbs.dispose();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new DisposableDelegate(() => {
|
|
43
|
+
breadcrumbs.dispose();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private _commands: CommandRegistry;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds a VS Code-style path breadcrumb to text editor toolbars. Each
|
|
52
|
+
* segment is clickable: clicking dispatches `xtralab:reveal-path` so
|
|
53
|
+
* any plugin that listens (today, the file browser) can surface the
|
|
54
|
+
* underlying folder or file. The breadcrumbs plugin therefore does not
|
|
55
|
+
* import the file browser at all — the JupyterLab command registry is
|
|
56
|
+
* the only seam between them.
|
|
57
|
+
*/
|
|
58
|
+
const plugin: JupyterFrontEndPlugin<void> = {
|
|
59
|
+
id: PLUGIN_ID,
|
|
60
|
+
description: 'Display the active file path in text editor toolbars.',
|
|
61
|
+
autoStart: true,
|
|
62
|
+
activate: (app: JupyterFrontEnd): void => {
|
|
63
|
+
app.docRegistry.addWidgetExtension(
|
|
64
|
+
EDITOR_FACTORY,
|
|
65
|
+
new EditorBreadcrumbsExtension(app.commands)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default plugin;
|