xtralab 0.1.2 → 0.3.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 +129 -46
- package/lib/fileBrowser/commands.d.ts +2 -2
- package/lib/fileBrowser/commands.js +2 -2
- package/lib/fileBrowser/fileBrowser.js +72 -2
- package/lib/fileBrowser/gitignore.d.ts +29 -0
- package/lib/fileBrowser/gitignore.js +74 -0
- package/lib/fileBrowser/icons.d.ts +4 -4
- package/lib/fileBrowser/icons.js +4 -4
- package/lib/fileBrowser/widget.d.ts +2 -2
- package/lib/fileBrowser/widget.js +2 -2
- 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/icons.d.ts +1 -1
- package/lib/git/icons.js +1 -1
- 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 +214 -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 +25 -3
- package/schema/launcher.json +70 -0
- package/schema/sidebar.json +57 -0
- package/src/fileBrowser/commands.ts +2 -2
- package/src/fileBrowser/fileBrowser.tsx +76 -2
- package/src/fileBrowser/gitignore.ts +86 -0
- package/src/fileBrowser/icons.ts +4 -4
- package/src/fileBrowser/widget.tsx +3 -3
- package/src/git/api.ts +20 -0
- package/src/git/commands.ts +19 -5
- package/src/git/diffWidget.tsx +109 -13
- package/src/git/icons.ts +1 -1
- 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 +462 -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/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 {};
|
package/lib/git/diffWidget.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { MainAreaWidget } from '@jupyterlab/apputils';
|
|
3
3
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
4
|
-
import { ReactWidget, undoIcon } from '@jupyterlab/ui-components';
|
|
4
|
+
import { ReactWidget, ToolbarButton, launchIcon, undoIcon } from '@jupyterlab/ui-components';
|
|
5
5
|
import { Signal } from '@lumino/signaling';
|
|
6
6
|
import { FileDiff } from '@pierre/diffs/react';
|
|
7
7
|
import { diffAcceptRejectHunk, parseDiffFromFile } from '@pierre/diffs';
|
|
@@ -13,6 +13,10 @@ import { buildNotebookDiff, NotebookDiffView } from './notebookDiff';
|
|
|
13
13
|
* style the embedded `@pierre/diffs` viewer hang off this class.
|
|
14
14
|
*/
|
|
15
15
|
export const DIFF_WIDGET_CSS_CLASS = 'jp-xtralab-DiffWidget';
|
|
16
|
+
export const PREVIEW_DIFF_WIDGET_ID = 'xtralab:diff:preview';
|
|
17
|
+
export function pinnedDiffWidgetId(change) {
|
|
18
|
+
return `xtralab:diff:pinned:${change.group}:${change.path}`;
|
|
19
|
+
}
|
|
16
20
|
/**
|
|
17
21
|
* `localStorage` key for the user's last-chosen split ratio. Persisted so
|
|
18
22
|
* the preference survives reloads — the library has no built-in way to
|
|
@@ -100,6 +104,9 @@ function writeStoredNotebookViewMode(mode) {
|
|
|
100
104
|
export function diffWidgetId(change) {
|
|
101
105
|
return `xtralab:diff:${change.group}:${change.path}`;
|
|
102
106
|
}
|
|
107
|
+
function toServerPath(repoPath, filePath) {
|
|
108
|
+
return repoPath.length > 0 ? PathExt.join(repoPath, filePath) : filePath;
|
|
109
|
+
}
|
|
103
110
|
/**
|
|
104
111
|
* Resolve the pair of git references whose `content` should appear on the
|
|
105
112
|
* left and right side of the diff for a given file change. Mirrors VS
|
|
@@ -128,6 +135,7 @@ export function resolveReferences(change) {
|
|
|
128
135
|
* read-only main-area widgets in the JupyterLab ecosystem.
|
|
129
136
|
*/
|
|
130
137
|
export function createGitDiffWidget(options) {
|
|
138
|
+
var _a;
|
|
131
139
|
// `widget` is captured by the `onEmpty` closure we pass into the content,
|
|
132
140
|
// but doesn't exist yet at construction time — declare it up front so the
|
|
133
141
|
// closure can read it once the widget is wired.
|
|
@@ -143,38 +151,81 @@ export function createGitDiffWidget(options) {
|
|
|
143
151
|
}
|
|
144
152
|
});
|
|
145
153
|
widget = new MainAreaWidget({ content });
|
|
146
|
-
widget.id =
|
|
154
|
+
widget.id =
|
|
155
|
+
options.pinned === true
|
|
156
|
+
? pinnedDiffWidgetId(options.change)
|
|
157
|
+
: PREVIEW_DIFF_WIDGET_ID;
|
|
147
158
|
widget.title.label = formatTitle(options.change);
|
|
148
159
|
widget.title.caption = options.change.path;
|
|
149
160
|
widget.title.closable = true;
|
|
150
161
|
widget.title.icon = getTreeIcon(options.change.path);
|
|
162
|
+
widget.title.className = options.pinned === true ? '' : 'jp-mod-preview';
|
|
163
|
+
const onPinned = (_a = options.onPinned) !== null && _a !== void 0 ? _a : (() => { });
|
|
151
164
|
widget.addClass(DIFF_WIDGET_CSS_CLASS);
|
|
152
165
|
// Wire the notebook view-mode selector into the `MainAreaWidget` toolbar
|
|
153
166
|
// — the proper home for document chrome controls. The toolbar starts
|
|
154
167
|
// hidden and becomes visible once the React content reports a parsed
|
|
155
168
|
// notebook diff via `setHasNotebookView`. Plain text / code diffs never
|
|
156
|
-
// flip that flag,
|
|
157
|
-
//
|
|
158
|
-
|
|
169
|
+
// flip that flag, and pin state can hide the toolbar again, so the host
|
|
170
|
+
// chrome tracks the active mode.
|
|
171
|
+
const pinButton = new ToolbarButton({
|
|
172
|
+
icon: launchIcon,
|
|
173
|
+
tooltip: 'Pin tab',
|
|
174
|
+
onClick: () => content.pin()
|
|
175
|
+
});
|
|
176
|
+
if (content.pinned) {
|
|
177
|
+
pinButton.hide();
|
|
178
|
+
}
|
|
179
|
+
widget.toolbar.addItem('pin', pinButton);
|
|
159
180
|
widget.toolbar.addItem('notebook-view-mode', new NotebookViewModeToolbarItem(content));
|
|
160
|
-
|
|
161
|
-
const onHasNotebookViewChanged = (_sender, value) => {
|
|
181
|
+
const syncToolbarVisibility = () => {
|
|
162
182
|
if (widget === null || widget.isDisposed) {
|
|
163
183
|
return;
|
|
164
184
|
}
|
|
165
|
-
if (
|
|
185
|
+
if (!content.pinned || content.hasNotebookView) {
|
|
166
186
|
widget.toolbar.show();
|
|
167
187
|
}
|
|
168
188
|
else {
|
|
169
189
|
widget.toolbar.hide();
|
|
170
190
|
}
|
|
171
191
|
};
|
|
192
|
+
const onHasNotebookViewChanged = () => {
|
|
193
|
+
if (widget === null || widget.isDisposed) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
syncToolbarVisibility();
|
|
197
|
+
};
|
|
198
|
+
const onPinnedChanged = (sender, value) => {
|
|
199
|
+
if (widget === null || widget.isDisposed) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
void sender;
|
|
203
|
+
if (value) {
|
|
204
|
+
pinButton.hide();
|
|
205
|
+
// Keep toolbar state and title styling consistent for promoted tabs.
|
|
206
|
+
onPinned(widget);
|
|
207
|
+
if (widget.isDisposed) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
widget.title.className = '';
|
|
211
|
+
}
|
|
212
|
+
syncToolbarVisibility();
|
|
213
|
+
};
|
|
172
214
|
content.hasNotebookViewChanged.connect(onHasNotebookViewChanged);
|
|
215
|
+
content.pinnedChanged.connect(onPinnedChanged);
|
|
216
|
+
syncToolbarVisibility();
|
|
173
217
|
widget.disposed.connect(() => {
|
|
174
218
|
content.hasNotebookViewChanged.disconnect(onHasNotebookViewChanged);
|
|
219
|
+
content.pinnedChanged.disconnect(onPinnedChanged);
|
|
175
220
|
});
|
|
176
221
|
return widget;
|
|
177
222
|
}
|
|
223
|
+
export function updateGitDiffWidget(widget, change) {
|
|
224
|
+
widget.content.setChange(change);
|
|
225
|
+
widget.title.label = formatTitle(change);
|
|
226
|
+
widget.title.caption = change.path;
|
|
227
|
+
widget.title.icon = getTreeIcon(change.path);
|
|
228
|
+
}
|
|
178
229
|
/**
|
|
179
230
|
* The actual React-rendered region of the diff viewer. Held in its own
|
|
180
231
|
* `ReactWidget` so the surrounding `MainAreaWidget` can manage chrome
|
|
@@ -193,7 +244,9 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
193
244
|
this._notebookViewModeChanged = new Signal(this);
|
|
194
245
|
this._hasNotebookView = false;
|
|
195
246
|
this._hasNotebookViewChanged = new Signal(this);
|
|
247
|
+
this._pinnedChanged = new Signal(this);
|
|
196
248
|
this._options = options;
|
|
249
|
+
this._pinned = options.pinned === true;
|
|
197
250
|
this._notebookViewMode = readStoredNotebookViewMode();
|
|
198
251
|
}
|
|
199
252
|
/** The change this widget renders, used by callers to identify the widget. */
|
|
@@ -236,6 +289,23 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
236
289
|
get hasNotebookViewChanged() {
|
|
237
290
|
return this._hasNotebookViewChanged;
|
|
238
291
|
}
|
|
292
|
+
get pinned() {
|
|
293
|
+
return this._pinned;
|
|
294
|
+
}
|
|
295
|
+
pin() {
|
|
296
|
+
if (this._pinned) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
this._pinned = true;
|
|
300
|
+
this._pinnedChanged.emit(true);
|
|
301
|
+
}
|
|
302
|
+
get pinnedChanged() {
|
|
303
|
+
return this._pinnedChanged;
|
|
304
|
+
}
|
|
305
|
+
setChange(change) {
|
|
306
|
+
this._options = { ...this._options, change };
|
|
307
|
+
this.update();
|
|
308
|
+
}
|
|
239
309
|
render() {
|
|
240
310
|
return React.createElement(DiffViewer, { ...this._options, widget: this });
|
|
241
311
|
}
|
|
@@ -355,8 +425,16 @@ function DiffViewer(props) {
|
|
|
355
425
|
}, [themeManager]);
|
|
356
426
|
React.useEffect(() => {
|
|
357
427
|
let cancelled = false;
|
|
428
|
+
const serverPath = toServerPath(repoPath, change.path);
|
|
429
|
+
setState({
|
|
430
|
+
loading: true,
|
|
431
|
+
metadata: null,
|
|
432
|
+
notebookDiff: null,
|
|
433
|
+
serverPath,
|
|
434
|
+
isBinary: false,
|
|
435
|
+
error: null
|
|
436
|
+
});
|
|
358
437
|
const { oldRef, newRef } = resolveReferences(change);
|
|
359
|
-
const serverPath = repoPath.length > 0 ? PathExt.join(repoPath, change.path) : change.path;
|
|
360
438
|
void (async () => {
|
|
361
439
|
var _a, _b, _c, _d;
|
|
362
440
|
try {
|
|
@@ -612,6 +690,9 @@ function NotebookViewModeControl(props) {
|
|
|
612
690
|
content.notebookViewModeChanged.disconnect(handler);
|
|
613
691
|
};
|
|
614
692
|
}, [content]);
|
|
693
|
+
if (!content.hasNotebookView) {
|
|
694
|
+
return React.createElement(React.Fragment, null);
|
|
695
|
+
}
|
|
615
696
|
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented", role: "tablist", "aria-label": "Notebook diff view mode" },
|
|
616
697
|
React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'notebook', "data-active": mode === 'notebook', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => content.setNotebookViewMode('notebook') }, "Notebook"),
|
|
617
698
|
React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'json', "data-active": mode === 'json', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => content.setNotebookViewMode('json') }, "JSON")));
|
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
|
package/lib/git/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ILayoutRestorer } from '@jupyterlab/application';
|
|
|
2
2
|
import { Dialog, ICommandPalette, IThemeManager, WidgetTracker, showDialog, showErrorMessage } from '@jupyterlab/apputils';
|
|
3
3
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
4
4
|
import { CommandIDs, registerGitCommands } from './commands';
|
|
5
|
-
import {
|
|
5
|
+
import { PREVIEW_DIFF_WIDGET_ID, pinnedDiffWidgetId } from './diffWidget';
|
|
6
6
|
import { GitPanel } from './panel';
|
|
7
7
|
const GIT_PLUGIN_ID = 'xtralab:git';
|
|
8
8
|
const GIT_DIFF_TRACKER_NAMESPACE = 'xtralab-git-diff';
|
|
@@ -34,14 +34,17 @@ const plugin = {
|
|
|
34
34
|
const tracker = new WidgetTracker({
|
|
35
35
|
namespace: GIT_DIFF_TRACKER_NAMESPACE
|
|
36
36
|
});
|
|
37
|
-
const findDiff = (change) => {
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
return
|
|
37
|
+
const findDiff = (change, pin = false) => {
|
|
38
|
+
const id = pin ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
|
|
39
|
+
const existing = tracker.find(widget => !widget.isDisposed && widget.id === id);
|
|
40
|
+
return existing !== null && existing !== void 0 ? existing : undefined;
|
|
41
41
|
};
|
|
42
42
|
const panel = new GitPanel({
|
|
43
|
-
openDiff: change => {
|
|
43
|
+
openDiff: (change, options) => {
|
|
44
44
|
const args = { repoPath, change };
|
|
45
|
+
if ((options === null || options === void 0 ? void 0 : options.pin) === true) {
|
|
46
|
+
args.pin = true;
|
|
47
|
+
}
|
|
45
48
|
void app.commands.execute(CommandIDs.openDiff, args);
|
|
46
49
|
},
|
|
47
50
|
confirm: async (title, body, accept) => {
|
|
@@ -67,6 +70,19 @@ const plugin = {
|
|
|
67
70
|
rendermime,
|
|
68
71
|
onChanged: () => panel.refresh(),
|
|
69
72
|
trackDiff: widget => tracker.add(widget),
|
|
73
|
+
onPinned: current => {
|
|
74
|
+
const existing = findDiff(current.content.change, true);
|
|
75
|
+
if (existing !== undefined &&
|
|
76
|
+
existing !== current &&
|
|
77
|
+
!existing.isDisposed) {
|
|
78
|
+
app.shell.activateById(existing.id);
|
|
79
|
+
current.close();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
current.id = pinnedDiffWidgetId(current.content.change);
|
|
83
|
+
current.title.className = '';
|
|
84
|
+
app.shell.activateById(current.id);
|
|
85
|
+
},
|
|
70
86
|
findDiff
|
|
71
87
|
});
|
|
72
88
|
}
|
|
@@ -8,7 +8,9 @@ import { IFileChange } from './tokens';
|
|
|
8
8
|
*/
|
|
9
9
|
export interface IGitPanelHandlers {
|
|
10
10
|
/** Open a diff viewer for the given file in the main area. */
|
|
11
|
-
openDiff(change: IFileChange
|
|
11
|
+
openDiff(change: IFileChange, options?: {
|
|
12
|
+
pin?: boolean;
|
|
13
|
+
}): void;
|
|
12
14
|
/** Show a confirmation dialog. Returns true when the user accepted. */
|
|
13
15
|
confirm(title: string, body: string, accept: string): Promise<boolean>;
|
|
14
16
|
/** Show an error dialog. */
|
|
@@ -169,7 +169,9 @@ export function GitPanelComponent(props) {
|
|
|
169
169
|
onClick: onUnstageAll
|
|
170
170
|
}
|
|
171
171
|
]
|
|
172
|
-
: undefined }, staged.map(change => (React.createElement(ChangeRow, { key: `${change.group}:${change.path}`, change: change, pendingAction: pendingActions[`${change.group}:${change.path}`], onClick:
|
|
172
|
+
: undefined }, staged.map(change => (React.createElement(ChangeRow, { key: `${change.group}:${change.path}`, change: change, pendingAction: pendingActions[`${change.group}:${change.path}`], onClick: event => handlers.openDiff(change, {
|
|
173
|
+
pin: event.ctrlKey || event.metaKey
|
|
174
|
+
}), actions: [
|
|
173
175
|
{
|
|
174
176
|
label: 'Unstage changes',
|
|
175
177
|
symbol: '−',
|
|
@@ -184,7 +186,9 @@ export function GitPanelComponent(props) {
|
|
|
184
186
|
onClick: onStageAll
|
|
185
187
|
}
|
|
186
188
|
]
|
|
187
|
-
: undefined }, unstaged.map(change => (React.createElement(ChangeRow, { key: `${change.group}:${change.path}`, change: change, pendingAction: pendingActions[`${change.group}:${change.path}`], onClick:
|
|
189
|
+
: undefined }, unstaged.map(change => (React.createElement(ChangeRow, { key: `${change.group}:${change.path}`, change: change, pendingAction: pendingActions[`${change.group}:${change.path}`], onClick: event => handlers.openDiff(change, {
|
|
190
|
+
pin: event.ctrlKey || event.metaKey
|
|
191
|
+
}), actions: [
|
|
188
192
|
{
|
|
189
193
|
label: 'Discard changes',
|
|
190
194
|
symbol: '⟲',
|
|
@@ -209,7 +213,6 @@ function PanelHeader(props) {
|
|
|
209
213
|
: null;
|
|
210
214
|
return (React.createElement("div", { className: "jp-xtralab-GitPanel-header" },
|
|
211
215
|
React.createElement("div", { className: "jp-xtralab-GitPanel-headerInfo" },
|
|
212
|
-
React.createElement("div", { className: "jp-xtralab-GitPanel-title" }, "Source Control"),
|
|
213
216
|
repoName !== null ? (React.createElement("div", { className: "jp-xtralab-GitPanel-repo", title: topLevel !== null && topLevel !== void 0 ? topLevel : '' }, repoName)) : null,
|
|
214
217
|
(result === null || result === void 0 ? void 0 : result.branch) !== undefined && (result === null || result === void 0 ? void 0 : result.branch) !== null ? (React.createElement("div", { className: "jp-xtralab-GitPanel-branch" },
|
|
215
218
|
result.branch,
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import fileBrowserPlugin from './fileBrowser';
|
|
2
2
|
import gitPlugin from './git';
|
|
3
|
+
import launcherPlugin from './launcher';
|
|
4
|
+
import menusPlugin from './menus';
|
|
5
|
+
import sidebarPlugin from './sidebar';
|
|
6
|
+
import statusBarPlugin from './statusBar';
|
|
3
7
|
/**
|
|
4
8
|
* Every plugin contributed by `xtralab`. The entry point of the
|
|
5
9
|
* labextension is an array because the package bundles several independent
|
|
@@ -9,6 +13,10 @@ import gitPlugin from './git';
|
|
|
9
13
|
*/
|
|
10
14
|
const plugins = [
|
|
11
15
|
fileBrowserPlugin,
|
|
12
|
-
gitPlugin
|
|
16
|
+
gitPlugin,
|
|
17
|
+
launcherPlugin,
|
|
18
|
+
menusPlugin,
|
|
19
|
+
sidebarPlugin,
|
|
20
|
+
statusBarPlugin
|
|
13
21
|
];
|
|
14
22
|
export default plugins;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
/**
|
|
3
|
+
* The shape of an agent card on the launcher. The `command` is the literal
|
|
4
|
+
* text typed into the new terminal session — interactive shells expand
|
|
5
|
+
* aliases, so users can point at `claude`, `cl`, or whatever runs their
|
|
6
|
+
* preferred wrapper.
|
|
7
|
+
*/
|
|
8
|
+
export interface IAgent {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
caption: string;
|
|
12
|
+
command: string;
|
|
13
|
+
icon: LabIcon;
|
|
14
|
+
rank: number;
|
|
15
|
+
/**
|
|
16
|
+
* When false, the launcher skips the `which`-based availability check for
|
|
17
|
+
* this entry — useful for aliases or shell functions that aren't on PATH
|
|
18
|
+
* but still resolve when typed in a real terminal. Defaults to true.
|
|
19
|
+
*/
|
|
20
|
+
requireAvailable: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Argv tokens spliced between `command` and a shell-quoted prompt when
|
|
23
|
+
* the user types one into the launcher's prompt box. The semantics:
|
|
24
|
+
*
|
|
25
|
+
* - `[]` → prompt is appended as a positional argument:
|
|
26
|
+
* `<command> 'PROMPT'`. (Used by claude, codex, gemini, vibe.)
|
|
27
|
+
* - `['-i']` / `['--prompt']` → the prompt is preceded by a flag:
|
|
28
|
+
* `<command> -i 'PROMPT'`. (Used by copilot, opencode.)
|
|
29
|
+
* - `undefined` → the agent does not accept an initial prompt. The
|
|
30
|
+
* launcher dims the agent's button while the prompt textarea is
|
|
31
|
+
* non-empty, so the user gets a clear signal rather than a silently
|
|
32
|
+
* dropped prompt.
|
|
33
|
+
*
|
|
34
|
+
* The prompt itself is always single-quoted with embedded single quotes
|
|
35
|
+
* escaped, so multi-line prompts and shell metacharacters are safe.
|
|
36
|
+
*/
|
|
37
|
+
promptArgs?: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The settings-side shape: every field except `id` is optional, so a user
|
|
41
|
+
* can override a single field on a default agent (e.g. swap the command for
|
|
42
|
+
* an alias) without restating the whole entry. New ids define brand-new
|
|
43
|
+
* agent cards.
|
|
44
|
+
*/
|
|
45
|
+
export interface IAgentSettings {
|
|
46
|
+
id: string;
|
|
47
|
+
label?: string;
|
|
48
|
+
caption?: string;
|
|
49
|
+
command?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Inline SVG for a custom agent's icon. Required for new ids whose icon
|
|
52
|
+
* isn't shipped with xtralab; ignored for default ids unless explicitly
|
|
53
|
+
* set (in which case it overrides the built-in).
|
|
54
|
+
*/
|
|
55
|
+
iconSvg?: string;
|
|
56
|
+
rank?: number;
|
|
57
|
+
/**
|
|
58
|
+
* When false, the agent is hidden from the launcher and the command
|
|
59
|
+
* palette. Defaults to true.
|
|
60
|
+
*/
|
|
61
|
+
enabled?: boolean;
|
|
62
|
+
/** See `IAgent.requireAvailable`. */
|
|
63
|
+
requireAvailable?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* See `IAgent.promptArgs`. Pass an empty array to mark the agent as
|
|
66
|
+
* accepting a positional prompt; pass an array like `["-p"]` to use a
|
|
67
|
+
* flag. Pass `null` to explicitly turn off prompt support for an agent
|
|
68
|
+
* that has it on by default.
|
|
69
|
+
*/
|
|
70
|
+
promptArgs?: string[] | null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Merge xtralab's defaults with the user's settings. Default entries keep
|
|
74
|
+
* their built-in fields unless explicitly overridden; user-only entries are
|
|
75
|
+
* appended. `enabled: false` filters an entry out of the result entirely
|
|
76
|
+
* (so callers don't need to check the flag again).
|
|
77
|
+
*/
|
|
78
|
+
export declare function mergeAgents(overrides: IAgentSettings[]): IAgent[];
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { LabIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
import { BUILTIN_AGENT_ICONS } from './icons';
|
|
3
|
+
/**
|
|
4
|
+
* Default agent cards shipped with xtralab. The list mirrors the eight
|
|
5
|
+
* first-class personas in `jupyter-ai-contrib/jupyter-ai-acp-client` (which
|
|
6
|
+
* is also where the icons come from), but uses the bare CLI names a user
|
|
7
|
+
* would type in a terminal rather than the ACP wrapper binaries that
|
|
8
|
+
* project spawns. Anything not on the user's `$PATH` is filtered out at
|
|
9
|
+
* activation time, so the wider list is harmless.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULTS = [
|
|
12
|
+
{
|
|
13
|
+
id: 'claude',
|
|
14
|
+
label: 'Claude',
|
|
15
|
+
caption: 'Start Claude Code in a new terminal.',
|
|
16
|
+
command: 'claude',
|
|
17
|
+
icon: BUILTIN_AGENT_ICONS.claude,
|
|
18
|
+
rank: 0,
|
|
19
|
+
requireAvailable: true,
|
|
20
|
+
promptArgs: []
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'codex',
|
|
24
|
+
label: 'Codex',
|
|
25
|
+
caption: 'Start the Codex CLI in a new terminal.',
|
|
26
|
+
command: 'codex',
|
|
27
|
+
icon: BUILTIN_AGENT_ICONS.codex,
|
|
28
|
+
rank: 1,
|
|
29
|
+
requireAvailable: true,
|
|
30
|
+
promptArgs: []
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'gemini',
|
|
34
|
+
label: 'Gemini',
|
|
35
|
+
caption: 'Start the Gemini CLI in a new terminal.',
|
|
36
|
+
command: 'gemini',
|
|
37
|
+
icon: BUILTIN_AGENT_ICONS.gemini,
|
|
38
|
+
rank: 2,
|
|
39
|
+
requireAvailable: true,
|
|
40
|
+
promptArgs: []
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'copilot',
|
|
44
|
+
label: 'Copilot',
|
|
45
|
+
caption: 'Start the GitHub Copilot CLI in a new terminal.',
|
|
46
|
+
command: 'copilot',
|
|
47
|
+
icon: BUILTIN_AGENT_ICONS.copilot,
|
|
48
|
+
rank: 3,
|
|
49
|
+
requireAvailable: true,
|
|
50
|
+
// `-i "PROMPT"` is the documented way to start interactive mode and
|
|
51
|
+
// auto-execute the prompt; `-p` exits after responding (non-interactive).
|
|
52
|
+
promptArgs: ['-i']
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'goose',
|
|
56
|
+
label: 'Goose',
|
|
57
|
+
caption: 'Start Goose in a new terminal.',
|
|
58
|
+
command: 'goose',
|
|
59
|
+
icon: BUILTIN_AGENT_ICONS.goose,
|
|
60
|
+
rank: 4,
|
|
61
|
+
requireAvailable: true
|
|
62
|
+
// `goose` does not accept an inline interactive prompt; users would
|
|
63
|
+
// need to type the prompt after `goose session` starts.
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'opencode',
|
|
67
|
+
label: 'OpenCode',
|
|
68
|
+
caption: 'Start OpenCode in a new terminal.',
|
|
69
|
+
command: 'opencode',
|
|
70
|
+
icon: BUILTIN_AGENT_ICONS.opencode,
|
|
71
|
+
rank: 5,
|
|
72
|
+
requireAvailable: true,
|
|
73
|
+
promptArgs: ['--prompt']
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'kiro',
|
|
77
|
+
label: 'Kiro',
|
|
78
|
+
caption: 'Start the Kiro CLI in a new terminal.',
|
|
79
|
+
command: 'kiro',
|
|
80
|
+
icon: BUILTIN_AGENT_ICONS.kiro,
|
|
81
|
+
rank: 6,
|
|
82
|
+
requireAvailable: true
|
|
83
|
+
// The Kiro chat session only takes initial prompts via the in-session
|
|
84
|
+
// `/chat new <prompt>` slash command, not a CLI argument.
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'mistral-vibe',
|
|
88
|
+
label: 'Mistral Vibe',
|
|
89
|
+
caption: 'Start Mistral Vibe in a new terminal.',
|
|
90
|
+
command: 'vibe',
|
|
91
|
+
icon: BUILTIN_AGENT_ICONS['mistral-vibe'],
|
|
92
|
+
rank: 7,
|
|
93
|
+
requireAvailable: true,
|
|
94
|
+
promptArgs: []
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
function resolveIcon(id, iconSvg) {
|
|
98
|
+
var _a;
|
|
99
|
+
if (iconSvg) {
|
|
100
|
+
return new LabIcon({ name: `xtralab:agent-custom-${id}`, svgstr: iconSvg });
|
|
101
|
+
}
|
|
102
|
+
return (_a = BUILTIN_AGENT_ICONS[id]) !== null && _a !== void 0 ? _a : terminalIcon;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Merge xtralab's defaults with the user's settings. Default entries keep
|
|
106
|
+
* their built-in fields unless explicitly overridden; user-only entries are
|
|
107
|
+
* appended. `enabled: false` filters an entry out of the result entirely
|
|
108
|
+
* (so callers don't need to check the flag again).
|
|
109
|
+
*/
|
|
110
|
+
export function mergeAgents(overrides) {
|
|
111
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
112
|
+
const overrideById = new Map(overrides.map(entry => [entry.id, entry]));
|
|
113
|
+
const merged = [];
|
|
114
|
+
for (const base of DEFAULTS) {
|
|
115
|
+
const override = overrideById.get(base.id);
|
|
116
|
+
if (!override) {
|
|
117
|
+
merged.push({ ...base });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
overrideById.delete(base.id);
|
|
121
|
+
if (override.enabled === false) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
merged.push({
|
|
125
|
+
id: base.id,
|
|
126
|
+
label: (_a = override.label) !== null && _a !== void 0 ? _a : base.label,
|
|
127
|
+
caption: (_b = override.caption) !== null && _b !== void 0 ? _b : base.caption,
|
|
128
|
+
command: (_c = override.command) !== null && _c !== void 0 ? _c : base.command,
|
|
129
|
+
icon: override.iconSvg
|
|
130
|
+
? resolveIcon(base.id, override.iconSvg)
|
|
131
|
+
: base.icon,
|
|
132
|
+
rank: (_d = override.rank) !== null && _d !== void 0 ? _d : base.rank,
|
|
133
|
+
requireAvailable: (_e = override.requireAvailable) !== null && _e !== void 0 ? _e : base.requireAvailable,
|
|
134
|
+
// `null` is the explicit way to opt out of an agent's default prompt
|
|
135
|
+
// support; an absent key keeps the default. `undefined` from `??` is
|
|
136
|
+
// pruned below.
|
|
137
|
+
promptArgs: override.promptArgs === null
|
|
138
|
+
? undefined
|
|
139
|
+
: ((_f = override.promptArgs) !== null && _f !== void 0 ? _f : base.promptArgs)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// What remains in `overrideById` are entries with ids that don't match a
|
|
143
|
+
// default — treat them as new agents the user is adding. Skip silently
|
|
144
|
+
// when a required field is missing rather than throwing; the settings
|
|
145
|
+
// schema validates the shape, so this is just a defensive fallback.
|
|
146
|
+
let nextRank = merged.reduce((max, agent) => Math.max(max, agent.rank), -1) + 1;
|
|
147
|
+
for (const entry of overrideById.values()) {
|
|
148
|
+
if (entry.enabled === false) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const label = (_g = entry.label) !== null && _g !== void 0 ? _g : entry.id;
|
|
152
|
+
const command = (_h = entry.command) !== null && _h !== void 0 ? _h : entry.id;
|
|
153
|
+
merged.push({
|
|
154
|
+
id: entry.id,
|
|
155
|
+
label,
|
|
156
|
+
caption: (_j = entry.caption) !== null && _j !== void 0 ? _j : `Start ${label} in a new terminal.`,
|
|
157
|
+
command,
|
|
158
|
+
icon: resolveIcon(entry.id, entry.iconSvg),
|
|
159
|
+
rank: (_k = entry.rank) !== null && _k !== void 0 ? _k : nextRank++,
|
|
160
|
+
requireAvailable: (_l = entry.requireAvailable) !== null && _l !== void 0 ? _l : true,
|
|
161
|
+
promptArgs: entry.promptArgs === null || entry.promptArgs === undefined
|
|
162
|
+
? undefined
|
|
163
|
+
: entry.promptArgs
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
merged.sort((a, b) => a.rank - b.rank);
|
|
167
|
+
return merged;
|
|
168
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hit the xtralab server extension's `which`-proxy endpoint and turn the
|
|
3
|
+
* response into a Set of commands that resolved to a real binary on the
|
|
4
|
+
* server's `$PATH`. The frontend uses this set to filter the launcher's
|
|
5
|
+
* agent cards.
|
|
6
|
+
*
|
|
7
|
+
* On any failure (server extension not loaded, network error, malformed
|
|
8
|
+
* response) the returned Set is `null` — callers should treat that as
|
|
9
|
+
* "availability unknown" and either fall back to showing all agents or
|
|
10
|
+
* surface the error, depending on context.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fetchAvailableCommands(commands: string[]): Promise<Set<string> | null>;
|