xtralab 0.14.1 → 0.15.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/lib/customPanel/index.d.ts +7 -0
- package/lib/customPanel/index.js +74 -0
- package/lib/fileBrowser/commands.js +13 -1
- package/lib/fileBrowser/contents.d.ts +11 -0
- package/lib/fileBrowser/contents.js +28 -0
- package/lib/fileBrowser/dragAndDrop.d.ts +66 -0
- package/lib/fileBrowser/dragAndDrop.js +115 -0
- package/lib/fileBrowser/fileBrowser.d.ts +2 -0
- package/lib/fileBrowser/fileBrowser.js +151 -197
- package/lib/fileBrowser/index.js +39 -6
- package/lib/fileBrowser/widget.d.ts +44 -5
- package/lib/fileBrowser/widget.js +153 -14
- package/lib/index.js +2 -0
- package/lib/launcher/agents.d.ts +1 -1
- package/lib/launcher/agents.js +15 -2
- package/lib/launcher/icons.js +15 -4
- package/lib/launcher/schemaDefaults.d.ts +2 -9
- package/lib/launcher/schemaDefaults.js +37 -13
- package/lib/sidebar/index.d.ts +7 -13
- package/lib/sidebar/index.js +80 -46
- package/lib/terminals/index.d.ts +5 -3
- package/lib/terminals/index.js +17 -6
- package/lib/terminals/widget.d.ts +29 -25
- package/lib/terminals/widget.js +150 -65
- package/package.json +1 -1
- package/schema/launcher.json +2 -2
- package/schema/sidebar.json +47 -7
- package/src/customPanel/index.ts +104 -0
- package/src/fileBrowser/commands.ts +14 -1
- package/src/fileBrowser/contents.ts +30 -0
- package/src/fileBrowser/dragAndDrop.ts +178 -0
- package/src/fileBrowser/fileBrowser.tsx +183 -225
- package/src/fileBrowser/index.ts +41 -6
- package/src/fileBrowser/widget.tsx +195 -14
- package/src/index.ts +2 -0
- package/src/launcher/agents.ts +16 -3
- package/src/launcher/icons.ts +16 -4
- package/src/launcher/schemaDefaults.ts +43 -13
- package/src/sidebar/index.ts +95 -52
- package/src/terminals/index.ts +24 -6
- package/src/terminals/widget.tsx +186 -87
- package/style/base.css +22 -34
- package/style/customPanel.css +9 -0
- package/style/index.js +1 -0
- package/style/terminals.css +19 -77
package/src/fileBrowser/index.ts
CHANGED
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
JupyterFrontEndPlugin
|
|
5
5
|
} from '@jupyterlab/application';
|
|
6
6
|
|
|
7
|
-
import { ICommandPalette } from '@jupyterlab/apputils';
|
|
7
|
+
import { ICommandPalette, IMovableSectionRegistry } from '@jupyterlab/apputils';
|
|
8
8
|
|
|
9
9
|
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
10
10
|
|
|
11
11
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
12
12
|
|
|
13
|
-
import { ITranslator } from '@jupyterlab/translation';
|
|
13
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
14
14
|
|
|
15
15
|
import { populateToolbar, registerCommands } from './commands';
|
|
16
|
-
import { XtralabFileBrowser } from './widget';
|
|
16
|
+
import { FILE_BROWSER_ID, XtralabFileBrowser } from './widget';
|
|
17
17
|
|
|
18
18
|
const PLUGIN_ID = 'xtralab:plugin';
|
|
19
19
|
|
|
@@ -27,21 +27,29 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
27
27
|
'A path-first file browser for JupyterLab built on @pierre/trees.',
|
|
28
28
|
autoStart: true,
|
|
29
29
|
requires: [IDocumentManager],
|
|
30
|
-
optional: [
|
|
30
|
+
optional: [
|
|
31
|
+
ILayoutRestorer,
|
|
32
|
+
ISettingRegistry,
|
|
33
|
+
ICommandPalette,
|
|
34
|
+
ITranslator,
|
|
35
|
+
IMovableSectionRegistry
|
|
36
|
+
],
|
|
31
37
|
activate: (
|
|
32
38
|
app: JupyterFrontEnd,
|
|
33
39
|
docManager: IDocumentManager,
|
|
34
40
|
restorer: ILayoutRestorer | null,
|
|
35
41
|
settingRegistry: ISettingRegistry | null,
|
|
36
42
|
palette: ICommandPalette | null,
|
|
37
|
-
translator: ITranslator | null
|
|
43
|
+
translator: ITranslator | null,
|
|
44
|
+
movableSections: IMovableSectionRegistry | null
|
|
38
45
|
): void => {
|
|
39
46
|
const browser = new XtralabFileBrowser({
|
|
40
47
|
contentsManager: app.serviceManager.contents,
|
|
41
48
|
docManager,
|
|
42
49
|
onOpenFile: (serverPath: string) => {
|
|
43
50
|
void app.commands.execute('docmanager:open', { path: serverPath });
|
|
44
|
-
}
|
|
51
|
+
},
|
|
52
|
+
translator: translator ?? undefined
|
|
45
53
|
});
|
|
46
54
|
// Ranks just under the Terminals panel in the left sidebar. The shipped
|
|
47
55
|
// `layout` setting assigns the default ranks (rank only, no area pin),
|
|
@@ -55,6 +63,33 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
55
63
|
registerCommands({ app, browser, docManager, palette, translator });
|
|
56
64
|
populateToolbar({ app, browser });
|
|
57
65
|
|
|
66
|
+
if (movableSections) {
|
|
67
|
+
const label = (translator ?? nullTranslator)
|
|
68
|
+
.load('jupyterlab')
|
|
69
|
+
.__('Files');
|
|
70
|
+
movableSections.registerSource(FILE_BROWSER_ID, label, browser);
|
|
71
|
+
movableSections.registerTarget(FILE_BROWSER_ID, label, browser);
|
|
72
|
+
|
|
73
|
+
// Only a user move should switch the sidebar, not the startup restoration.
|
|
74
|
+
let settled = false;
|
|
75
|
+
void app.restored.then(() => {
|
|
76
|
+
settled = true;
|
|
77
|
+
});
|
|
78
|
+
browser.contentChanged.connect(() => {
|
|
79
|
+
if (browser.isEmpty) {
|
|
80
|
+
if (browser.parent) {
|
|
81
|
+
browser.parent = null;
|
|
82
|
+
}
|
|
83
|
+
} else if (!browser.parent) {
|
|
84
|
+
app.shell.add(browser, 'left', { rank: 2 });
|
|
85
|
+
if (settled) {
|
|
86
|
+
app.shell.activateById(browser.id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
browser.announceSections();
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
if (settingRegistry) {
|
|
59
94
|
settingRegistry.load(PLUGIN_ID).catch(reason => {
|
|
60
95
|
console.error('Failed to load settings for xtralab.', reason);
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
IMovableSectionDestination,
|
|
5
|
+
IMovableSectionSource,
|
|
6
|
+
ISectionEntry
|
|
7
|
+
} from '@jupyterlab/apputils';
|
|
3
8
|
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
4
9
|
import { Contents } from '@jupyterlab/services';
|
|
5
|
-
import {
|
|
10
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
11
|
+
import {
|
|
12
|
+
AccordionToolbar,
|
|
13
|
+
PanelWithToolbar,
|
|
14
|
+
ReactWidget,
|
|
15
|
+
Toolbar
|
|
16
|
+
} from '@jupyterlab/ui-components';
|
|
6
17
|
import { Signal, ISignal } from '@lumino/signaling';
|
|
7
|
-
import { PanelLayout, Widget } from '@lumino/widgets';
|
|
18
|
+
import { AccordionPanel, PanelLayout, Widget } from '@lumino/widgets';
|
|
8
19
|
|
|
9
20
|
import { FileBrowserComponent } from './fileBrowser';
|
|
10
21
|
import { xtralabFileBrowserIcon } from './icons';
|
|
@@ -12,20 +23,30 @@ import { xtralabFileBrowserIcon } from './icons';
|
|
|
12
23
|
export const FILE_BROWSER_ID = 'xtralab:file-browser';
|
|
13
24
|
|
|
14
25
|
/**
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
* Persisted by the movable-sections plugin, so it must stay stable.
|
|
27
|
+
*/
|
|
28
|
+
const TREE_SECTION_ID = 'xtralab-file-tree-section';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Context menu selectors hang off this class, so it sits on the section and
|
|
32
|
+
* keeps matching once the tree moves to another panel.
|
|
19
33
|
*/
|
|
20
34
|
const FILE_BROWSER_CSS_CLASS = 'jp-xtralab-FileBrowser';
|
|
21
35
|
|
|
36
|
+
const PANEL_CSS_CLASS = 'jp-xtralab-FileBrowserPanel';
|
|
37
|
+
|
|
22
38
|
const TOOLBAR_CSS_CLASS = 'jp-xtralab-FileBrowser-toolbar';
|
|
23
39
|
const CONTENT_CSS_CLASS = 'jp-xtralab-FileBrowser-content';
|
|
24
40
|
|
|
41
|
+
const ACCORDION_CSS_CLASS = 'jp-xtralab-FileBrowser-accordion';
|
|
42
|
+
|
|
43
|
+
const MOVABLE_SECTION_CLASS = 'jp-movable-section';
|
|
44
|
+
|
|
25
45
|
interface IXtralabFileBrowserOptions {
|
|
26
46
|
contentsManager: Contents.IManager;
|
|
27
47
|
docManager: IDocumentManager;
|
|
28
48
|
onOpenFile?: (serverPath: string) => void;
|
|
49
|
+
translator?: ITranslator;
|
|
29
50
|
}
|
|
30
51
|
|
|
31
52
|
/**
|
|
@@ -155,39 +176,56 @@ export interface IXtralabFileBrowser {
|
|
|
155
176
|
* Toggle the filter box above the tree.
|
|
156
177
|
*/
|
|
157
178
|
toggleFileFilter(): void;
|
|
179
|
+
|
|
180
|
+
readonly sectionNode: HTMLElement;
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
/**
|
|
161
|
-
* Lumino widget
|
|
162
|
-
*
|
|
163
|
-
*
|
|
184
|
+
* Lumino widget hosting the React-based `@pierre/trees` file browser. The tree
|
|
185
|
+
* and its toolbar form one movable "Files" section, and sections from other
|
|
186
|
+
* panels can be moved in below it.
|
|
164
187
|
*/
|
|
165
|
-
export class XtralabFileBrowser
|
|
188
|
+
export class XtralabFileBrowser
|
|
189
|
+
extends Widget
|
|
190
|
+
implements
|
|
191
|
+
IXtralabFileBrowser,
|
|
192
|
+
IMovableSectionSource,
|
|
193
|
+
IMovableSectionDestination
|
|
194
|
+
{
|
|
166
195
|
constructor(options: IXtralabFileBrowserOptions) {
|
|
167
196
|
super();
|
|
168
|
-
const layout = (this.layout = new PanelLayout());
|
|
197
|
+
const layout = (this._panelLayout = this.layout = new PanelLayout());
|
|
169
198
|
|
|
170
199
|
this._contentsManager = options.contentsManager;
|
|
171
200
|
this._docManager = options.docManager;
|
|
172
201
|
this._onOpenFile = options.onOpenFile;
|
|
202
|
+
this._translator = options.translator;
|
|
173
203
|
this.id = FILE_BROWSER_ID;
|
|
174
204
|
this.title.icon = xtralabFileBrowserIcon;
|
|
175
205
|
this.title.caption = 'xtralab File Browser';
|
|
176
|
-
this.addClass(
|
|
206
|
+
this.addClass(PANEL_CSS_CLASS);
|
|
177
207
|
|
|
178
208
|
this._toolbar = new Toolbar();
|
|
179
209
|
this._toolbar.addClass(TOOLBAR_CSS_CLASS);
|
|
180
210
|
this._toolbar.node.setAttribute('aria-label', 'file browser');
|
|
181
|
-
layout.addWidget(this._toolbar);
|
|
182
211
|
|
|
183
212
|
this._content = new XtralabFileTreeContent({
|
|
184
213
|
contentsManager: this._contentsManager,
|
|
185
214
|
docManager: this._docManager,
|
|
186
215
|
onOpenFile: this._onOpenFile,
|
|
216
|
+
translator: this._translator,
|
|
187
217
|
browser: this
|
|
188
218
|
});
|
|
189
219
|
this._content.addClass(CONTENT_CSS_CLASS);
|
|
190
|
-
|
|
220
|
+
|
|
221
|
+
this._section = new PanelWithToolbar({ toolbar: this._toolbar });
|
|
222
|
+
this._section.id = TREE_SECTION_ID;
|
|
223
|
+
// Only an accordion host draws this; a plain `PanelLayout` shows no title.
|
|
224
|
+
this._section.title.label = 'Files';
|
|
225
|
+
this._section.addClass(FILE_BROWSER_CSS_CLASS);
|
|
226
|
+
this._section.addWidget(this._toolbar);
|
|
227
|
+
this._section.addWidget(this._content);
|
|
228
|
+
layout.addWidget(this._section);
|
|
191
229
|
}
|
|
192
230
|
|
|
193
231
|
get contentsManager(): Contents.IManager {
|
|
@@ -280,9 +318,145 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
|
|
|
280
318
|
this.setFileFilterVisible(!this._fileFilterVisible);
|
|
281
319
|
}
|
|
282
320
|
|
|
321
|
+
get sectionNode(): HTMLElement {
|
|
322
|
+
return this._section.node;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
get isEmpty(): boolean {
|
|
326
|
+
return this._panelLayout.widgets.length === 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
get contentChanged(): ISignal<this, void> {
|
|
330
|
+
return this._contentChanged;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
get sectionAdded(): ISignal<this, ISectionEntry> {
|
|
334
|
+
return this._sectionAdded;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
get accordionPanel(): AccordionPanel | null {
|
|
338
|
+
return this._accordion;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
get sections(): ReadonlyArray<Widget> {
|
|
342
|
+
if (!this._accordion) {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
return this._accordion.widgets.filter(w => w !== this._section);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* The toolbar is the section's title node: it is the tree's header row at
|
|
350
|
+
* home, and an accordion host renders it inside the section title.
|
|
351
|
+
*/
|
|
352
|
+
getSections(): ReadonlyArray<ISectionEntry> {
|
|
353
|
+
return this._isSectionHome()
|
|
354
|
+
? [
|
|
355
|
+
{
|
|
356
|
+
id: TREE_SECTION_ID,
|
|
357
|
+
titleNode: this._toolbar.node,
|
|
358
|
+
widget: this._section
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
: [];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
removeSectionById(sectionId: string): Widget | null {
|
|
365
|
+
if (sectionId !== TREE_SECTION_ID || !this._isSectionHome()) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
this._section.parent = null;
|
|
369
|
+
if (this._accordion && this._accordion.widgets.length === 0) {
|
|
370
|
+
this._accordion.dispose();
|
|
371
|
+
this._accordion = null;
|
|
372
|
+
}
|
|
373
|
+
// Away from home the toolbar must not offer "Move to …" as well;
|
|
374
|
+
// `getSections` re-marks it on the way back.
|
|
375
|
+
this._toolbar.node.classList.remove(MOVABLE_SECTION_CLASS);
|
|
376
|
+
this._contentChanged.emit();
|
|
377
|
+
return this._section;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
reinsertSection(widget: Widget): void {
|
|
381
|
+
if (this._accordion) {
|
|
382
|
+
this._accordion.insertWidget(0, widget);
|
|
383
|
+
} else {
|
|
384
|
+
this._panelLayout.addWidget(widget);
|
|
385
|
+
this._restoreToolbar();
|
|
386
|
+
}
|
|
387
|
+
this._contentChanged.emit();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The move plugin reconciles persisted moves only against sections announced
|
|
392
|
+
* after `registerSource`, and this panel builds its section in the constructor.
|
|
393
|
+
*/
|
|
394
|
+
announceSections(): void {
|
|
395
|
+
for (const entry of this.getSections()) {
|
|
396
|
+
this._sectionAdded.emit(entry);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The first hosted section moves the tree into an accordion.
|
|
402
|
+
*/
|
|
403
|
+
addSection(widget: Widget): void {
|
|
404
|
+
if (!this._accordion) {
|
|
405
|
+
const accordion = new AccordionPanel({
|
|
406
|
+
layout: AccordionToolbar.createLayout({})
|
|
407
|
+
});
|
|
408
|
+
accordion.addClass(ACCORDION_CSS_CLASS);
|
|
409
|
+
if (this._section.parent === this) {
|
|
410
|
+
accordion.addWidget(this._section);
|
|
411
|
+
}
|
|
412
|
+
accordion.addWidget(widget);
|
|
413
|
+
this._accordion = accordion;
|
|
414
|
+
this._panelLayout.addWidget(accordion);
|
|
415
|
+
} else {
|
|
416
|
+
this._accordion.addWidget(widget);
|
|
417
|
+
}
|
|
418
|
+
this._contentChanged.emit();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
removeSectionWidget(widget: Widget): void {
|
|
422
|
+
if (!this._accordion || widget.parent !== this._accordion) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
widget.parent = null;
|
|
426
|
+
const remaining = this._accordion.widgets;
|
|
427
|
+
const treeOnly = remaining.length === 1 && remaining[0] === this._section;
|
|
428
|
+
if (treeOnly || remaining.length === 0) {
|
|
429
|
+
if (treeOnly) {
|
|
430
|
+
if (this._section.isHidden) {
|
|
431
|
+
this._section.show();
|
|
432
|
+
}
|
|
433
|
+
this._panelLayout.addWidget(this._section);
|
|
434
|
+
this._restoreToolbar();
|
|
435
|
+
}
|
|
436
|
+
this._accordion.dispose();
|
|
437
|
+
this._accordion = null;
|
|
438
|
+
}
|
|
439
|
+
this._contentChanged.emit();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private _isSectionHome(): boolean {
|
|
443
|
+
const parent = this._section.parent;
|
|
444
|
+
return parent === this || (!!this._accordion && parent === this._accordion);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* An accordion host moves the toolbar node into the section title and leaves
|
|
449
|
+
* it orphaned on detach; re-parenting makes the layout attach it again.
|
|
450
|
+
*/
|
|
451
|
+
private _restoreToolbar(): void {
|
|
452
|
+
this._toolbar.parent = null;
|
|
453
|
+
this._section.insertWidget(0, this._toolbar);
|
|
454
|
+
}
|
|
455
|
+
|
|
283
456
|
private _contentsManager: Contents.IManager;
|
|
284
457
|
private _docManager: IDocumentManager;
|
|
285
458
|
private _onOpenFile: ((serverPath: string) => void) | undefined;
|
|
459
|
+
private _translator: ITranslator | undefined;
|
|
286
460
|
private _selectedPaths: readonly string[] = [];
|
|
287
461
|
private _selectionChanged = new Signal<this, readonly string[]>(this);
|
|
288
462
|
private _refreshRequested = new Signal<this, void>(this);
|
|
@@ -294,12 +468,18 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
|
|
|
294
468
|
private _fileFilterVisibleChanged = new Signal<this, boolean>(this);
|
|
295
469
|
private _toolbar: Toolbar;
|
|
296
470
|
private _content: XtralabFileTreeContent;
|
|
471
|
+
private _section: PanelWithToolbar;
|
|
472
|
+
private _panelLayout: PanelLayout;
|
|
473
|
+
private _accordion: AccordionPanel | null = null;
|
|
474
|
+
private _contentChanged = new Signal<this, void>(this);
|
|
475
|
+
private _sectionAdded = new Signal<this, ISectionEntry>(this);
|
|
297
476
|
}
|
|
298
477
|
|
|
299
478
|
interface IFileTreeContentOptions {
|
|
300
479
|
contentsManager: Contents.IManager;
|
|
301
480
|
docManager: IDocumentManager;
|
|
302
481
|
onOpenFile?: (serverPath: string) => void;
|
|
482
|
+
translator?: ITranslator;
|
|
303
483
|
browser: XtralabFileBrowser;
|
|
304
484
|
}
|
|
305
485
|
|
|
@@ -319,6 +499,7 @@ class XtralabFileTreeContent extends ReactWidget {
|
|
|
319
499
|
contentsManager={this._options.contentsManager}
|
|
320
500
|
docManager={this._options.docManager}
|
|
321
501
|
onOpenFile={this._options.onOpenFile}
|
|
502
|
+
translator={this._options.translator}
|
|
322
503
|
widget={this._options.browser}
|
|
323
504
|
/>
|
|
324
505
|
);
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import aboutPlugin from './about';
|
|
|
4
4
|
import agentSessionsPlugin from './agentSessions';
|
|
5
5
|
import askAgentPlugin from './askAgent';
|
|
6
6
|
import commandBarPlugin from './commandBar';
|
|
7
|
+
import customPanelPlugin from './customPanel';
|
|
7
8
|
import fileBrowserPlugin from './fileBrowser';
|
|
8
9
|
import fileTypeIconsPlugin from './fileTypeIcons';
|
|
9
10
|
import editorBreadcrumbsPlugin from './editorBreadcrumbs';
|
|
@@ -36,6 +37,7 @@ const plugins: JupyterFrontEndPlugin<unknown>[] = [
|
|
|
36
37
|
agentSessionsPlugin,
|
|
37
38
|
askAgentPlugin,
|
|
38
39
|
commandBarPlugin,
|
|
40
|
+
customPanelPlugin,
|
|
39
41
|
editorBreadcrumbsPlugin,
|
|
40
42
|
editorIndentPlugin,
|
|
41
43
|
fileBrowserPlugin,
|
package/src/launcher/agents.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface IAgent {
|
|
|
26
26
|
* the user types one into the launcher's prompt box. The semantics:
|
|
27
27
|
*
|
|
28
28
|
* - `[]` → prompt is appended as a positional argument:
|
|
29
|
-
* `<command> 'PROMPT'`. (Used by claude, codex, vibe.)
|
|
29
|
+
* `<command> 'PROMPT'`. (Used by claude, codex, vibe, pi.)
|
|
30
30
|
* - `['-i']` / `['--prompt']` → the prompt is preceded by a flag:
|
|
31
31
|
* `<command> -i 'PROMPT'`. (Used by copilot, opencode.)
|
|
32
32
|
* - `undefined` → the agent does not accept an initial prompt. The
|
|
@@ -86,8 +86,9 @@ export interface IAgentSettings {
|
|
|
86
86
|
* icons come from), but use the bare CLI names a user would type in a
|
|
87
87
|
* terminal rather than the ACP wrapper binaries that project spawns.
|
|
88
88
|
* Antigravity (Google's agent-first IDE/CLI, `agy`) is the Google agent.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
89
|
+
* Pi (https://pi.dev, `earendil-works/pi`) is a minimal extensible coding
|
|
90
|
+
* agent; its mark comes straight from its site. Anything not on the user's
|
|
91
|
+
* `$PATH` is filtered out at activation time, so the wider list is harmless.
|
|
91
92
|
*/
|
|
92
93
|
const DEFAULTS: IAgent[] = [
|
|
93
94
|
{
|
|
@@ -179,6 +180,18 @@ const DEFAULTS: IAgent[] = [
|
|
|
179
180
|
rank: 7,
|
|
180
181
|
requireAvailable: true,
|
|
181
182
|
promptArgs: []
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'pi',
|
|
186
|
+
label: 'Pi',
|
|
187
|
+
caption: 'Start Pi in a new terminal.',
|
|
188
|
+
command: 'pi',
|
|
189
|
+
icon: BUILTIN_AGENT_ICONS.pi,
|
|
190
|
+
rank: 8,
|
|
191
|
+
requireAvailable: true,
|
|
192
|
+
// `pi [messages...]` starts the interactive TUI and sends the messages
|
|
193
|
+
// as the first user prompt, so the positional form applies.
|
|
194
|
+
promptArgs: []
|
|
182
195
|
}
|
|
183
196
|
];
|
|
184
197
|
|
package/src/launcher/icons.ts
CHANGED
|
@@ -6,11 +6,14 @@ import { LabIcon } from '@jupyterlab/ui-components';
|
|
|
6
6
|
* (`jupyter_ai_acp_client/static/*.svg`, BSD-3-Clause); Antigravity post-dates
|
|
7
7
|
* that set, so its mark is the official glyph from `@lobehub/icons-static-svg`
|
|
8
8
|
* (MIT) — we ship the brand-colored `antigravity-color` variant so Antigravity
|
|
9
|
-
* reads as a multi-color Google mark.
|
|
9
|
+
* reads as a multi-color Google mark. Pi's mark is the official `logo-auto.svg`
|
|
10
|
+
* from https://pi.dev (MIT, `earendil-works/pi`). Small adjustments are applied
|
|
10
11
|
* to play nicely with JupyterLab themes:
|
|
11
|
-
* - Monochrome marks (Codex, Copilot, Goose) use `currentColor` so they
|
|
12
|
+
* - Monochrome marks (Codex, Copilot, Goose, Pi) use `currentColor` so they
|
|
12
13
|
* inherit the surrounding launcher card's text color rather than
|
|
13
|
-
* hard-coding black, which disappears on the dark theme.
|
|
14
|
+
* hard-coding black, which disappears on the dark theme. For Pi this
|
|
15
|
+
* replaces the upstream `prefers-color-scheme` style block, which follows
|
|
16
|
+
* the OS scheme rather than the active JupyterLab theme.
|
|
14
17
|
* - The Goose mark upstream is rendered on a white rounded rect; we drop
|
|
15
18
|
* the rect so the silhouette can sit on the launcher card directly.
|
|
16
19
|
* - SVG ids that would collide if two copies of the same artwork ended up
|
|
@@ -179,6 +182,14 @@ const opencodeIcon = new LabIcon({
|
|
|
179
182
|
</svg>`
|
|
180
183
|
});
|
|
181
184
|
|
|
185
|
+
const piIcon = new LabIcon({
|
|
186
|
+
name: 'xtralab:agent-pi',
|
|
187
|
+
svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" fill="currentColor">
|
|
188
|
+
<path fill-rule="evenodd" d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"/>
|
|
189
|
+
<path d="M517.36 400H634.72V634.72H517.36Z"/>
|
|
190
|
+
</svg>`
|
|
191
|
+
});
|
|
192
|
+
|
|
182
193
|
const neovimIcon = new LabIcon({
|
|
183
194
|
name: 'xtralab:editor-neovim',
|
|
184
195
|
svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#57A143">
|
|
@@ -207,7 +218,8 @@ export const BUILTIN_AGENT_ICONS: Record<string, LabIcon> = {
|
|
|
207
218
|
goose: gooseIcon,
|
|
208
219
|
kiro: kiroIcon,
|
|
209
220
|
'mistral-vibe': mistralVibeIcon,
|
|
210
|
-
opencode: opencodeIcon
|
|
221
|
+
opencode: opencodeIcon,
|
|
222
|
+
pi: piIcon
|
|
211
223
|
};
|
|
212
224
|
|
|
213
225
|
/**
|
|
@@ -10,21 +10,39 @@ import { defaultEditorSettings } from './editors';
|
|
|
10
10
|
export const LAUNCHER_PLUGIN_ID = 'xtralab:launcher';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
* re-registering (the registry rejects a duplicate transformer).
|
|
13
|
+
* Registries that already have the transform (duplicates are rejected).
|
|
15
14
|
*/
|
|
16
15
|
const registered = new WeakSet<ISettingRegistry>();
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
* Merge sparse user entries over the built-in defaults, keyed by `id`.
|
|
19
|
+
*/
|
|
20
|
+
function mergeSettingsById<
|
|
21
|
+
T extends { id: string; enabled?: boolean; requireAvailable?: boolean }
|
|
22
|
+
>(defaults: T[], user: PartialJSONValue | undefined): T[] {
|
|
23
|
+
const entries = Array.isArray(user) ? (user as unknown as T[]) : [];
|
|
24
|
+
const overrideById = new Map(entries.map(entry => [entry.id, entry]));
|
|
25
|
+
const merged = defaults.map(base => {
|
|
26
|
+
const override = overrideById.get(base.id);
|
|
27
|
+
if (!override) {
|
|
28
|
+
return base;
|
|
29
|
+
}
|
|
30
|
+
overrideById.delete(base.id);
|
|
31
|
+
return { ...base, ...override };
|
|
32
|
+
});
|
|
33
|
+
merged.push(...overrideById.values());
|
|
34
|
+
// Item schema defaults are applied before this transform runs; without the
|
|
35
|
+
// refill, sparse entries render with unchecked boxes.
|
|
36
|
+
return merged.map(entry => ({
|
|
37
|
+
...entry,
|
|
38
|
+
enabled: entry.enabled ?? true,
|
|
39
|
+
requireAvailable: entry.requireAvailable ?? true
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Must run before the first `load(LAUNCHER_PLUGIN_ID)` (the schema sets
|
|
45
|
+
* `jupyter.lab.transform: true`); idempotent across callers.
|
|
28
46
|
*/
|
|
29
47
|
export function registerLauncherSchemaDefaults(
|
|
30
48
|
settingRegistry: ISettingRegistry
|
|
@@ -35,11 +53,23 @@ export function registerLauncherSchemaDefaults(
|
|
|
35
53
|
registered.add(settingRegistry);
|
|
36
54
|
|
|
37
55
|
settingRegistry.transform(LAUNCHER_PLUGIN_ID, {
|
|
56
|
+
compose: plugin => {
|
|
57
|
+
const { user } = plugin.data;
|
|
58
|
+
const composite = { ...plugin.data.composite };
|
|
59
|
+
composite.agents = mergeSettingsById(
|
|
60
|
+
defaultAgentSettings(),
|
|
61
|
+
user.agents
|
|
62
|
+
) as unknown as PartialJSONValue;
|
|
63
|
+
composite.editors = mergeSettingsById(
|
|
64
|
+
defaultEditorSettings(),
|
|
65
|
+
user.editors
|
|
66
|
+
) as unknown as PartialJSONValue;
|
|
67
|
+
plugin.data = { composite, user };
|
|
68
|
+
return plugin;
|
|
69
|
+
},
|
|
38
70
|
fetch: plugin => {
|
|
39
71
|
const properties = plugin.schema.properties;
|
|
40
72
|
if (properties) {
|
|
41
|
-
// The settings shapes are plain JSON by construction, but their named
|
|
42
|
-
// interfaces lack an index signature, so cast at this TS→JSON boundary.
|
|
43
73
|
if (properties.agents) {
|
|
44
74
|
properties.agents.default =
|
|
45
75
|
defaultAgentSettings() as unknown as PartialJSONValue;
|