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/lib/terminals/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ILayoutRestorer } from '@jupyterlab/application';
|
|
2
|
-
import { Dialog, showDialog } from '@jupyterlab/apputils';
|
|
2
|
+
import { Dialog, IMovableSectionRegistry, showDialog } from '@jupyterlab/apputils';
|
|
3
3
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
4
4
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
5
5
|
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
@@ -29,9 +29,11 @@ const PLUGIN_ID = 'xtralab:terminals';
|
|
|
29
29
|
*
|
|
30
30
|
* Clicking a row activates the existing tab if one is open, or reopens
|
|
31
31
|
* the session in a fresh terminal widget (`terminal:open`). The inline
|
|
32
|
-
* `×` button shuts the session down on the server. The
|
|
33
|
-
* `+` button and a stop button (the
|
|
34
|
-
* once, after a confirmation).
|
|
32
|
+
* `×` button shuts the session down on the server. The Terminals
|
|
33
|
+
* section's title toolbar carries a `+` button and a stop button (the
|
|
34
|
+
* latter shuts every session down at once, after a confirmation).
|
|
35
|
+
*
|
|
36
|
+
* The list is one movable section, and other panels' sections can move in.
|
|
35
37
|
*
|
|
36
38
|
* The `+` button drops down a menu — built from the launcher's shared
|
|
37
39
|
* `IAgentRegistry` — listing each available agent (Claude, Codex, …)
|
|
@@ -57,9 +59,10 @@ const plugin = {
|
|
|
57
59
|
IAgentRegistry,
|
|
58
60
|
IEditorRegistry,
|
|
59
61
|
IAgentSessions,
|
|
60
|
-
ISettingRegistry
|
|
62
|
+
ISettingRegistry,
|
|
63
|
+
IMovableSectionRegistry
|
|
61
64
|
],
|
|
62
|
-
activate: (app, tracker, restorer, translator, agentRegistry, editorRegistry, agentSessions, settingRegistry) => {
|
|
65
|
+
activate: (app, tracker, restorer, translator, agentRegistry, editorRegistry, agentSessions, settingRegistry, movableSections) => {
|
|
63
66
|
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
|
|
64
67
|
// Resolve a running agent/editor identifier to an icon: the matching
|
|
65
68
|
// agent's logo, the matching editor's logo (Neovim/Vim), or the plain
|
|
@@ -224,6 +227,14 @@ const plugin = {
|
|
|
224
227
|
if (restorer) {
|
|
225
228
|
restorer.add(panel, panel.id);
|
|
226
229
|
}
|
|
230
|
+
void app.restored.then(() => {
|
|
231
|
+
panel.expandOwnSection();
|
|
232
|
+
});
|
|
233
|
+
if (movableSections) {
|
|
234
|
+
movableSections.registerSource(PLUGIN_ID, trans.__('Terminals'), panel);
|
|
235
|
+
movableSections.registerTarget(PLUGIN_ID, trans.__('Terminals'), panel);
|
|
236
|
+
panel.announceSections();
|
|
237
|
+
}
|
|
227
238
|
// The latest-activity line under each agent row is on by default; honour
|
|
228
239
|
// the `showAgentActivity` setting so it can be turned off. Read on load and
|
|
229
240
|
// re-applied on every change.
|
|
@@ -1,35 +1,39 @@
|
|
|
1
|
+
import { IMovableSectionDestination, IMovableSectionSource, ISectionEntry } from '@jupyterlab/apputils';
|
|
1
2
|
import { type TranslationBundle } from '@jupyterlab/translation';
|
|
2
|
-
import { LabIcon,
|
|
3
|
-
import
|
|
3
|
+
import { LabIcon, SidePanel } from '@jupyterlab/ui-components';
|
|
4
|
+
import { ISignal } from '@lumino/signaling';
|
|
5
|
+
import { AccordionPanel, Widget } from '@lumino/widgets';
|
|
4
6
|
import { SessionRegistry } from './model';
|
|
5
7
|
/**
|
|
6
|
-
* Left-sidebar panel listing every running terminal session
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* running so the user can jump back to a backgrounded agent — including
|
|
10
|
-
* sessions whose tab has been closed but which are still alive on the
|
|
11
|
-
* server. Each row is labelled with the session's title and, for a session
|
|
12
|
-
* running a coding agent, a smaller line below it showing the agent's latest
|
|
13
|
-
* line of output (from {@link SessionRegistry.activityFor}).
|
|
14
|
-
*
|
|
15
|
-
* Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
|
|
16
|
-
* whenever the registry emits `stateChanged`, so it tracks
|
|
17
|
-
* `runningChanged` and the agent-title cache without any local state.
|
|
18
|
-
* Click behavior (activate vs. reopen, shutdown, new terminal) is
|
|
19
|
-
* delegated to plugin-supplied callbacks so the widget never imports
|
|
20
|
-
* `app`.
|
|
8
|
+
* Left-sidebar panel listing every running terminal session, including the
|
|
9
|
+
* sessions whose tab is closed but which are still alive on the server. The
|
|
10
|
+
* list is a single movable "Terminals" section.
|
|
21
11
|
*/
|
|
22
|
-
export declare class RunningTerminals extends
|
|
12
|
+
export declare class RunningTerminals extends SidePanel implements IMovableSectionSource, IMovableSectionDestination {
|
|
23
13
|
constructor(options: RunningTerminals.IOptions);
|
|
14
|
+
get accordionPanel(): AccordionPanel;
|
|
15
|
+
get sectionAdded(): ISignal<this, ISectionEntry>;
|
|
16
|
+
get sections(): ReadonlyArray<Widget>;
|
|
17
|
+
getSections(): ReadonlyArray<ISectionEntry>;
|
|
18
|
+
removeSectionById(sectionId: string): Widget | null;
|
|
19
|
+
reinsertSection(widget: Widget): void;
|
|
20
|
+
addSection(widget: Widget): void;
|
|
21
|
+
removeSectionWidget(widget: Widget): void;
|
|
22
|
+
/**
|
|
23
|
+
* The layout restorer stores a section as collapsed whenever its panel is off
|
|
24
|
+
* screen at save time, so it can come back collapsed without the user asking.
|
|
25
|
+
*/
|
|
26
|
+
expandOwnSection(): void;
|
|
27
|
+
/**
|
|
28
|
+
* The move plugin reconciles persisted moves only against sections announced
|
|
29
|
+
* after `registerSource`, and this panel builds its section in the constructor.
|
|
30
|
+
*/
|
|
31
|
+
announceSections(): void;
|
|
24
32
|
dispose(): void;
|
|
25
|
-
|
|
33
|
+
private _sectionEntry;
|
|
26
34
|
private _registry;
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private _onActivate;
|
|
30
|
-
private _onShutdown;
|
|
31
|
-
private _onShutdownAll;
|
|
32
|
-
private _onCreate;
|
|
35
|
+
private _section;
|
|
36
|
+
private _sectionAdded;
|
|
33
37
|
}
|
|
34
38
|
export declare namespace RunningTerminals {
|
|
35
39
|
interface IOptions {
|
package/lib/terminals/widget.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
2
|
-
import { addIcon, closeIcon, ReactWidget, stopIcon, terminalIcon, UseSignal } from '@jupyterlab/ui-components';
|
|
2
|
+
import { addIcon, closeIcon, PanelWithToolbar, ReactWidget, SidePanel, stopIcon, terminalIcon, ToolbarButton, UseSignal } from '@jupyterlab/ui-components';
|
|
3
|
+
import { Signal } from '@lumino/signaling';
|
|
3
4
|
import * as React from 'react';
|
|
4
5
|
/**
|
|
5
6
|
* Id of the panel widget. Used for layout restoration and as the handle
|
|
@@ -7,37 +8,112 @@ import * as React from 'react';
|
|
|
7
8
|
*/
|
|
8
9
|
const RUNNING_TERMINALS_ID = 'xtralab-running-terminals';
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* launcher *starts* sessions, this panel surfaces the ones already
|
|
13
|
-
* running so the user can jump back to a backgrounded agent — including
|
|
14
|
-
* sessions whose tab has been closed but which are still alive on the
|
|
15
|
-
* server. Each row is labelled with the session's title and, for a session
|
|
16
|
-
* running a coding agent, a smaller line below it showing the agent's latest
|
|
17
|
-
* line of output (from {@link SessionRegistry.activityFor}).
|
|
18
|
-
*
|
|
19
|
-
* Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
|
|
20
|
-
* whenever the registry emits `stateChanged`, so it tracks
|
|
21
|
-
* `runningChanged` and the agent-title cache without any local state.
|
|
22
|
-
* Click behavior (activate vs. reopen, shutdown, new terminal) is
|
|
23
|
-
* delegated to plugin-supplied callbacks so the widget never imports
|
|
24
|
-
* `app`.
|
|
11
|
+
* Id of the accordion section holding the terminals list. Persisted by the
|
|
12
|
+
* movable-sections plugin, so it must stay stable across releases.
|
|
25
13
|
*/
|
|
26
|
-
|
|
14
|
+
const TERMINALS_SECTION_ID = 'xtralab-terminals-section';
|
|
15
|
+
/**
|
|
16
|
+
* Left-sidebar panel listing every running terminal session, including the
|
|
17
|
+
* sessions whose tab is closed but which are still alive on the server. The
|
|
18
|
+
* list is a single movable "Terminals" section.
|
|
19
|
+
*/
|
|
20
|
+
export class RunningTerminals extends SidePanel {
|
|
27
21
|
constructor(options) {
|
|
28
22
|
var _a;
|
|
29
23
|
super();
|
|
24
|
+
this._sectionAdded = new Signal(this);
|
|
30
25
|
this._registry = options.registry;
|
|
31
|
-
|
|
32
|
-
this._iconForCommand = options.iconForCommand;
|
|
33
|
-
this._onActivate = options.onActivate;
|
|
34
|
-
this._onShutdown = options.onShutdown;
|
|
35
|
-
this._onShutdownAll = options.onShutdownAll;
|
|
36
|
-
this._onCreate = options.onCreate;
|
|
26
|
+
const trans = (_a = options.trans) !== null && _a !== void 0 ? _a : nullTranslator.load('jupyterlab');
|
|
37
27
|
this.id = RUNNING_TERMINALS_ID;
|
|
38
28
|
this.title.icon = terminalIcon;
|
|
39
|
-
this.title.caption =
|
|
29
|
+
this.title.caption = trans.__('Running Terminals');
|
|
40
30
|
this.addClass('jp-xtralab-Terminals');
|
|
31
|
+
const section = (this._section = new PanelWithToolbar());
|
|
32
|
+
section.id = TERMINALS_SECTION_ID;
|
|
33
|
+
section.title.label = trans.__('Terminals');
|
|
34
|
+
section.addClass('jp-xtralab-Terminals-section');
|
|
35
|
+
const shutdownAll = new ToolbarButton({
|
|
36
|
+
icon: stopIcon,
|
|
37
|
+
onClick: options.onShutdownAll,
|
|
38
|
+
tooltip: trans.__('Shut Down All Terminals'),
|
|
39
|
+
enabled: this._registry.sessionNames().length > 0
|
|
40
|
+
});
|
|
41
|
+
this._registry.stateChanged.connect(() => {
|
|
42
|
+
shutdownAll.enabled = this._registry.sessionNames().length > 0;
|
|
43
|
+
});
|
|
44
|
+
section.toolbar.addItem('shutdown-all', shutdownAll);
|
|
45
|
+
const newTerminal = new ToolbarButton({
|
|
46
|
+
icon: addIcon,
|
|
47
|
+
onClick: () => {
|
|
48
|
+
// Anchor whatever the plugin shows (usually an agent menu) to the
|
|
49
|
+
// button's bottom-left, in the viewport coordinates `Menu.open` wants.
|
|
50
|
+
const rect = newTerminal.node.getBoundingClientRect();
|
|
51
|
+
options.onCreate({ x: rect.left, y: rect.bottom });
|
|
52
|
+
},
|
|
53
|
+
tooltip: trans.__('New Terminal')
|
|
54
|
+
});
|
|
55
|
+
section.toolbar.addItem('new-terminal', newTerminal);
|
|
56
|
+
section.addWidget(new TerminalsListing({
|
|
57
|
+
registry: this._registry,
|
|
58
|
+
trans,
|
|
59
|
+
iconForCommand: options.iconForCommand,
|
|
60
|
+
onActivate: options.onActivate,
|
|
61
|
+
onShutdown: options.onShutdown
|
|
62
|
+
}));
|
|
63
|
+
this.addWidget(section);
|
|
64
|
+
}
|
|
65
|
+
get accordionPanel() {
|
|
66
|
+
return this.content;
|
|
67
|
+
}
|
|
68
|
+
get sectionAdded() {
|
|
69
|
+
return this._sectionAdded;
|
|
70
|
+
}
|
|
71
|
+
get sections() {
|
|
72
|
+
return this.accordionPanel.widgets.filter(w => w !== this._section);
|
|
73
|
+
}
|
|
74
|
+
getSections() {
|
|
75
|
+
const entry = this._sectionEntry();
|
|
76
|
+
return entry ? [entry] : [];
|
|
77
|
+
}
|
|
78
|
+
removeSectionById(sectionId) {
|
|
79
|
+
if (sectionId !== TERMINALS_SECTION_ID ||
|
|
80
|
+
this._section.parent !== this.content) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
this._section.parent = null;
|
|
84
|
+
return this._section;
|
|
85
|
+
}
|
|
86
|
+
reinsertSection(widget) {
|
|
87
|
+
this.addWidget(widget);
|
|
88
|
+
}
|
|
89
|
+
addSection(widget) {
|
|
90
|
+
this.addWidget(widget);
|
|
91
|
+
}
|
|
92
|
+
removeSectionWidget(widget) {
|
|
93
|
+
if (widget.parent === this.content) {
|
|
94
|
+
widget.parent = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The layout restorer stores a section as collapsed whenever its panel is off
|
|
99
|
+
* screen at save time, so it can come back collapsed without the user asking.
|
|
100
|
+
*/
|
|
101
|
+
expandOwnSection() {
|
|
102
|
+
const accordion = this.accordionPanel;
|
|
103
|
+
const index = Array.from(accordion.widgets).indexOf(this._section);
|
|
104
|
+
if (index >= 0) {
|
|
105
|
+
accordion.expand(index);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The move plugin reconciles persisted moves only against sections announced
|
|
110
|
+
* after `registerSource`, and this panel builds its section in the constructor.
|
|
111
|
+
*/
|
|
112
|
+
announceSections() {
|
|
113
|
+
const entry = this._sectionEntry();
|
|
114
|
+
if (entry) {
|
|
115
|
+
this._sectionAdded.emit(entry);
|
|
116
|
+
}
|
|
41
117
|
}
|
|
42
118
|
dispose() {
|
|
43
119
|
if (this.isDisposed) {
|
|
@@ -48,54 +124,63 @@ export class RunningTerminals extends ReactWidget {
|
|
|
48
124
|
this._registry.dispose();
|
|
49
125
|
super.dispose();
|
|
50
126
|
}
|
|
127
|
+
_sectionEntry() {
|
|
128
|
+
const accordion = this.accordionPanel;
|
|
129
|
+
const index = Array.from(accordion.widgets).indexOf(this._section);
|
|
130
|
+
const titleNode = accordion.titles[index];
|
|
131
|
+
if (index < 0 || !titleNode) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
id: TERMINALS_SECTION_ID,
|
|
136
|
+
titleNode,
|
|
137
|
+
widget: this._section
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The body of the Terminals section.
|
|
143
|
+
*/
|
|
144
|
+
class TerminalsListing extends ReactWidget {
|
|
145
|
+
constructor(options) {
|
|
146
|
+
super();
|
|
147
|
+
this._options = options;
|
|
148
|
+
this.addClass('jp-xtralab-Terminals-listing');
|
|
149
|
+
}
|
|
51
150
|
render() {
|
|
52
|
-
return (React.createElement(UseSignal, { signal: this.
|
|
151
|
+
return (React.createElement(UseSignal, { signal: this._options.registry.stateChanged }, () => (React.createElement(RunningTerminalsComponent, { registry: this._options.registry, trans: this._options.trans, iconForCommand: this._options.iconForCommand, onActivate: this._options.onActivate, onShutdown: this._options.onShutdown }))));
|
|
53
152
|
}
|
|
54
153
|
}
|
|
55
154
|
function RunningTerminalsComponent(props) {
|
|
56
|
-
const { registry, trans, iconForCommand, onActivate, onShutdown
|
|
155
|
+
const { registry, trans, iconForCommand, onActivate, onShutdown } = props;
|
|
57
156
|
const names = registry.sessionNames();
|
|
58
157
|
// The session whose terminal is the current widget in the main area, so its
|
|
59
158
|
// row can be highlighted. `null` when the current tab is a notebook or any
|
|
60
159
|
// other non-terminal widget.
|
|
61
160
|
const currentName = registry.currentSessionName();
|
|
62
|
-
return (React.createElement("div", { className: "jp-xtralab-Terminals-body" },
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// smaller line under the title; `null` for rows with nothing to
|
|
88
|
-
// surface (no agent running, or no open tab to read a live buffer).
|
|
89
|
-
const activity = registry.activityFor(name);
|
|
90
|
-
return (React.createElement("li", { key: name, className: isCurrent
|
|
91
|
-
? 'jp-xtralab-Terminals-item jp-mod-current'
|
|
92
|
-
: 'jp-xtralab-Terminals-item' },
|
|
93
|
-
React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-activate", onClick: () => onActivate(name), title: tooltip, "aria-label": tooltip, "aria-current": isCurrent || undefined },
|
|
94
|
-
React.createElement(RowIcon, { tag: "span", className: "jp-xtralab-Terminals-item-icon", verticalAlign: "middle" }),
|
|
95
|
-
React.createElement("span", { className: "jp-xtralab-Terminals-item-text" },
|
|
96
|
-
React.createElement("span", { className: "jp-xtralab-Terminals-item-label" }, label),
|
|
97
|
-
activity ? (React.createElement("span", { className: "jp-xtralab-Terminals-item-detail" }, activity)) : null)),
|
|
98
|
-
React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-close", onClick: () => onShutdown(name), title: trans.__('Shut down %1', label), "aria-label": trans.__('Shut down %1', label) },
|
|
99
|
-
React.createElement(closeIcon.react, { tag: "span", verticalAlign: "middle" }))));
|
|
100
|
-
})))));
|
|
161
|
+
return (React.createElement("div", { className: "jp-xtralab-Terminals-body" }, names.length === 0 ? (React.createElement("p", { className: "jp-xtralab-Terminals-empty" }, trans.__('No running terminals.'))) : (React.createElement("ul", { className: "jp-xtralab-Terminals-list" }, names.map(name => {
|
|
162
|
+
const label = registry.labelFor(name);
|
|
163
|
+
const hasWidget = registry.widgetFor(name) !== null;
|
|
164
|
+
const tooltip = hasWidget
|
|
165
|
+
? trans.__('Activate %1', label)
|
|
166
|
+
: trans.__('Reopen %1', label);
|
|
167
|
+
// The running agent's logo (e.g. Claude), or the plain terminal
|
|
168
|
+
// icon when nothing recognised is running in the session.
|
|
169
|
+
const RowIcon = iconForCommand(registry.agentCommandFor(name)).react;
|
|
170
|
+
const isCurrent = name === currentName;
|
|
171
|
+
// The latest line of output from the session's agent, shown as a
|
|
172
|
+
// smaller line under the title; `null` for rows with nothing to
|
|
173
|
+
// surface (no agent running, or no open tab to read a live buffer).
|
|
174
|
+
const activity = registry.activityFor(name);
|
|
175
|
+
return (React.createElement("li", { key: name, className: isCurrent
|
|
176
|
+
? 'jp-xtralab-Terminals-item jp-mod-current'
|
|
177
|
+
: 'jp-xtralab-Terminals-item' },
|
|
178
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-activate", onClick: () => onActivate(name), title: tooltip, "aria-label": tooltip, "aria-current": isCurrent || undefined },
|
|
179
|
+
React.createElement(RowIcon, { tag: "span", className: "jp-xtralab-Terminals-item-icon", verticalAlign: "middle" }),
|
|
180
|
+
React.createElement("span", { className: "jp-xtralab-Terminals-item-text" },
|
|
181
|
+
React.createElement("span", { className: "jp-xtralab-Terminals-item-label" }, label),
|
|
182
|
+
activity ? (React.createElement("span", { className: "jp-xtralab-Terminals-item-detail" }, activity)) : null)),
|
|
183
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-close", onClick: () => onShutdown(name), title: trans.__('Shut down %1', label), "aria-label": trans.__('Shut down %1', label) },
|
|
184
|
+
React.createElement(closeIcon.react, { tag: "span", verticalAlign: "middle" }))));
|
|
185
|
+
})))));
|
|
101
186
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtralab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser, renders jupyterlab-git's text, notebook and image diffs with its own viewers, and applies a quieter default workspace configuration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
package/schema/launcher.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"properties": {
|
|
9
9
|
"agents": {
|
|
10
10
|
"title": "Agents",
|
|
11
|
-
"description": "Override or extend the built-in list of agents shown on the launcher. Each entry must include an `id`. When the id matches a built-in (claude, codex, antigravity, copilot, goose, opencode, kiro, mistral-vibe), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define brand-new cards. Set `enabled: false` to hide a default. Changing a built-in's `command` (for example to a shell alias like `ccm`) keeps the card visible even though the alias isn't on PATH; use `requireAvailable: false` only when you want to skip the check without changing the command. The full built-in list is shown as the default below, so you can copy an entry and tweak it in place.",
|
|
11
|
+
"description": "Override or extend the built-in list of agents shown on the launcher. Each entry must include an `id`. When the id matches a built-in (claude, codex, antigravity, copilot, goose, opencode, kiro, mistral-vibe, pi), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define brand-new cards. Set `enabled: false` to hide a default. Changing a built-in's `command` (for example to a shell alias like `ccm`) keeps the card visible even though the alias isn't on PATH; use `requireAvailable: false` only when you want to skip the check without changing the command. The full built-in list is shown as the default below, so you can copy an entry and tweak it in place. Saved entries are merged into the built-in list by id, so this settings view always shows every built-in agent alongside your overrides.",
|
|
12
12
|
"type": "array",
|
|
13
13
|
"default": [],
|
|
14
14
|
"items": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
"editors": {
|
|
71
71
|
"title": "Editors",
|
|
72
|
-
"description": "Override or extend the terminal editors offered in the launcher's Open section (and recognized in the Terminals panel). Each entry must include an `id`. When the id matches a built-in (nvim, vim), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define additional editors. The launcher shows a single tile — the first editor, by `rank`, whose `command` is on PATH (Neovim before Vim by default). Set `enabled: false` to hide a built-in; disable both to remove the tile entirely. Changing a built-in's `command` (for example to a shell alias) keeps the tile visible even though the alias isn't on PATH; use `requireAvailable: false` only when you want to skip the check without changing the command. The full built-in list is shown as the default below, so you can copy an entry and tweak it in place.",
|
|
72
|
+
"description": "Override or extend the terminal editors offered in the launcher's Open section (and recognized in the Terminals panel). Each entry must include an `id`. When the id matches a built-in (nvim, vim), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define additional editors. The launcher shows a single tile — the first editor, by `rank`, whose `command` is on PATH (Neovim before Vim by default). Set `enabled: false` to hide a built-in; disable both to remove the tile entirely. Changing a built-in's `command` (for example to a shell alias) keeps the tile visible even though the alias isn't on PATH; use `requireAvailable: false` only when you want to skip the check without changing the command. The full built-in list is shown as the default below, so you can copy an entry and tweak it in place. Saved entries are merged into the built-in list by id, so this settings view always shows every built-in editor alongside your overrides.",
|
|
73
73
|
"type": "array",
|
|
74
74
|
"default": [],
|
|
75
75
|
"items": {
|
package/schema/sidebar.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "xtralab sidebar",
|
|
3
|
-
"description": "Visibility of
|
|
3
|
+
"description": "Visibility of individual sidebar tabs.",
|
|
4
4
|
"jupyter.lab.shortcuts": [],
|
|
5
5
|
"jupyter.lab.menus": {
|
|
6
6
|
"main": [
|
|
@@ -17,16 +17,32 @@
|
|
|
17
17
|
"type": "submenu",
|
|
18
18
|
"rank": 12.5,
|
|
19
19
|
"submenu": {
|
|
20
|
-
"id": "jp-mainmenu-view-appearance-
|
|
21
|
-
"label": "
|
|
20
|
+
"id": "jp-mainmenu-view-appearance-sidebars",
|
|
21
|
+
"label": "Sidebars",
|
|
22
22
|
"items": [
|
|
23
23
|
{
|
|
24
|
-
"command": "xtralab:toggle-
|
|
24
|
+
"command": "xtralab:toggle-terminals",
|
|
25
25
|
"rank": 1
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
|
-
"command": "xtralab:toggle-
|
|
28
|
+
"command": "xtralab:toggle-file-browser",
|
|
29
29
|
"rank": 2
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"command": "xtralab:toggle-git-panel",
|
|
33
|
+
"rank": 3
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"command": "xtralab:toggle-default-filebrowser",
|
|
37
|
+
"rank": 4
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"command": "xtralab:toggle-running-sessions",
|
|
41
|
+
"rank": 5
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"command": "xtralab:toggle-search-replace",
|
|
45
|
+
"rank": 6
|
|
30
46
|
}
|
|
31
47
|
]
|
|
32
48
|
}
|
|
@@ -40,16 +56,40 @@
|
|
|
40
56
|
},
|
|
41
57
|
"type": "object",
|
|
42
58
|
"properties": {
|
|
59
|
+
"showTerminals": {
|
|
60
|
+
"type": "boolean",
|
|
61
|
+
"title": "Show the terminals panel",
|
|
62
|
+
"description": "When false, hides the xtralab Terminals (`xtralab-running-terminals`) tab from the sidebar.",
|
|
63
|
+
"default": true
|
|
64
|
+
},
|
|
65
|
+
"showFileBrowser": {
|
|
66
|
+
"type": "boolean",
|
|
67
|
+
"title": "Show the xtralab file browser",
|
|
68
|
+
"description": "When false, hides the xtralab file browser (`xtralab:file-browser`) tab from the sidebar. The default JupyterLab file browser is unaffected.",
|
|
69
|
+
"default": true
|
|
70
|
+
},
|
|
71
|
+
"showGitPanel": {
|
|
72
|
+
"type": "boolean",
|
|
73
|
+
"title": "Show the Git panel",
|
|
74
|
+
"description": "When false, hides the `jp-git-sessions` (Git) tab from the sidebar.",
|
|
75
|
+
"default": true
|
|
76
|
+
},
|
|
43
77
|
"showDefaultFileBrowser": {
|
|
44
78
|
"type": "boolean",
|
|
45
79
|
"title": "Show the default JupyterLab file browser",
|
|
46
|
-
"description": "When false, hides the default `filebrowser` tab from the
|
|
80
|
+
"description": "When false, hides the default `filebrowser` tab from the sidebar. The xtralab file browser is unaffected.",
|
|
47
81
|
"default": true
|
|
48
82
|
},
|
|
49
83
|
"showRunningSessions": {
|
|
50
84
|
"type": "boolean",
|
|
51
85
|
"title": "Show running terminals and kernels",
|
|
52
|
-
"description": "When false, hides the `jp-running-sessions` (Running Terminals and Kernels) tab from the
|
|
86
|
+
"description": "When false, hides the `jp-running-sessions` (Running Terminals and Kernels) tab from the sidebar.",
|
|
87
|
+
"default": true
|
|
88
|
+
},
|
|
89
|
+
"showSearchReplace": {
|
|
90
|
+
"type": "boolean",
|
|
91
|
+
"title": "Show search and replace",
|
|
92
|
+
"description": "When false, hides the `jp-search-replace` (Search and Replace) tab from the sidebar.",
|
|
53
93
|
"default": true
|
|
54
94
|
}
|
|
55
95
|
},
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
import {
|
|
6
|
+
IMovableSectionDestination,
|
|
7
|
+
IMovableSectionRegistry
|
|
8
|
+
} from '@jupyterlab/apputils';
|
|
9
|
+
import {
|
|
10
|
+
ITranslator,
|
|
11
|
+
nullTranslator,
|
|
12
|
+
type TranslationBundle
|
|
13
|
+
} from '@jupyterlab/translation';
|
|
14
|
+
import { SidePanel, tableRowsIcon } from '@jupyterlab/ui-components';
|
|
15
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
16
|
+
import { AccordionPanel, Widget } from '@lumino/widgets';
|
|
17
|
+
|
|
18
|
+
const PLUGIN_ID = 'xtralab:custom-panel';
|
|
19
|
+
|
|
20
|
+
const CUSTOM_PANEL_ID = 'xtralab-custom-panel';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A sidebar panel holding only the sections moved in from other panels.
|
|
24
|
+
*/
|
|
25
|
+
class CustomPanel extends SidePanel implements IMovableSectionDestination {
|
|
26
|
+
constructor(options: { trans: TranslationBundle }) {
|
|
27
|
+
super();
|
|
28
|
+
this.id = CUSTOM_PANEL_ID;
|
|
29
|
+
this.title.icon = tableRowsIcon;
|
|
30
|
+
this.title.caption = options.trans.__('Custom Panel');
|
|
31
|
+
this.addClass('jp-xtralab-CustomPanel');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get accordionPanel(): AccordionPanel {
|
|
35
|
+
return this.content as AccordionPanel;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get sections(): ReadonlyArray<Widget> {
|
|
39
|
+
return this.content.widgets;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get sectionCountChanged(): ISignal<this, number> {
|
|
43
|
+
return this._sectionCountChanged;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
addSection(widget: Widget): void {
|
|
47
|
+
this.addWidget(widget);
|
|
48
|
+
this._sectionCountChanged.emit(this.content.widgets.length);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
removeSectionWidget(widget: Widget): void {
|
|
52
|
+
if (widget.parent !== this.content) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
widget.parent = null;
|
|
56
|
+
this._sectionCountChanged.emit(this.content.widgets.length);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private _sectionCountChanged = new Signal<this, number>(this);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Registers "Custom Panel", which takes a sidebar tab only while it hosts a
|
|
64
|
+
* section.
|
|
65
|
+
*/
|
|
66
|
+
const plugin: JupyterFrontEndPlugin<void> = {
|
|
67
|
+
id: PLUGIN_ID,
|
|
68
|
+
description:
|
|
69
|
+
'A user-assembled sidebar panel hosting sections moved in from other panels; hidden while empty.',
|
|
70
|
+
autoStart: true,
|
|
71
|
+
optional: [IMovableSectionRegistry, ITranslator],
|
|
72
|
+
activate: (
|
|
73
|
+
app: JupyterFrontEnd,
|
|
74
|
+
registry: IMovableSectionRegistry | null,
|
|
75
|
+
translator: ITranslator | null
|
|
76
|
+
): void => {
|
|
77
|
+
if (!registry) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
81
|
+
const panel = new CustomPanel({ trans });
|
|
82
|
+
|
|
83
|
+
// Only a user move should switch the sidebar, not the startup restoration.
|
|
84
|
+
let restored = false;
|
|
85
|
+
void app.restored.then(() => {
|
|
86
|
+
restored = true;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
panel.sectionCountChanged.connect((_, count) => {
|
|
90
|
+
if (count > 0 && !panel.parent) {
|
|
91
|
+
app.shell.add(panel, 'left', { rank: 3 });
|
|
92
|
+
if (restored) {
|
|
93
|
+
app.shell.activateById(panel.id);
|
|
94
|
+
}
|
|
95
|
+
} else if (count === 0 && panel.parent) {
|
|
96
|
+
panel.parent = null;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
registry.registerTarget(PLUGIN_ID, trans.__('Custom Panel'), panel);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default plugin;
|
|
@@ -512,11 +512,24 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
512
512
|
// point of view. An empty path is the "workspace root" gesture —
|
|
513
513
|
// there is no tree row for the root itself, so the browser is asked
|
|
514
514
|
// to scroll back to the top and clear its selection instead.
|
|
515
|
+
// The tree is movable, so surface whichever sidebar widget holds it.
|
|
516
|
+
const activateTreeHost = (): void => {
|
|
517
|
+
for (const area of ['left', 'right']) {
|
|
518
|
+
for (const widget of app.shell.widgets(area)) {
|
|
519
|
+
if (widget.node.contains(browser.sectionNode)) {
|
|
520
|
+
app.shell.activateById(widget.id);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
app.shell.activateById(FILE_BROWSER_ID);
|
|
526
|
+
};
|
|
527
|
+
|
|
515
528
|
commands.addCommand(CommandIDs.revealPath, {
|
|
516
529
|
label: trans.__('Reveal in File Browser'),
|
|
517
530
|
execute: (args: ReadonlyPartialJSONObject) => {
|
|
518
531
|
const path = (args.path as string | undefined) ?? '';
|
|
519
|
-
|
|
532
|
+
activateTreeHost();
|
|
520
533
|
if (path.length === 0) {
|
|
521
534
|
browser.scrollToRoot();
|
|
522
535
|
} else {
|
|
@@ -17,6 +17,36 @@ export function toCanonicalPath(child: Contents.IModel): string {
|
|
|
17
17
|
return child.type === 'directory' ? `${child.path}${DIR_SUFFIX}` : child.path;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Compute the canonical parent path for a canonical path. Returns
|
|
22
|
+
* {@link ROOT_LOAD_KEY} for top-level entries.
|
|
23
|
+
*/
|
|
24
|
+
export function parentOf(canonicalPath: string): string {
|
|
25
|
+
const trimmed = canonicalPath.endsWith(DIR_SUFFIX)
|
|
26
|
+
? canonicalPath.slice(0, -DIR_SUFFIX.length)
|
|
27
|
+
: canonicalPath;
|
|
28
|
+
const idx = trimmed.lastIndexOf(DIR_SUFFIX);
|
|
29
|
+
if (idx < 0) {
|
|
30
|
+
return ROOT_LOAD_KEY;
|
|
31
|
+
}
|
|
32
|
+
return `${trimmed.slice(0, idx)}${DIR_SUFFIX}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The last segment of a canonical path. Directories keep their trailing
|
|
37
|
+
* slash, so appending the result to a canonical directory path yields a
|
|
38
|
+
* canonical path again.
|
|
39
|
+
*/
|
|
40
|
+
export function canonicalBasename(canonicalPath: string): string {
|
|
41
|
+
const isDir = canonicalPath.endsWith(DIR_SUFFIX);
|
|
42
|
+
const trimmed = isDir
|
|
43
|
+
? canonicalPath.slice(0, -DIR_SUFFIX.length)
|
|
44
|
+
: canonicalPath;
|
|
45
|
+
const idx = trimmed.lastIndexOf(DIR_SUFFIX);
|
|
46
|
+
const base = idx < 0 ? trimmed : trimmed.slice(idx + DIR_SUFFIX.length);
|
|
47
|
+
return isDir ? `${base}${DIR_SUFFIX}` : base;
|
|
48
|
+
}
|
|
49
|
+
|
|
20
50
|
/**
|
|
21
51
|
* Strip the trailing slash that `@pierre/trees` uses for canonical directory
|
|
22
52
|
* paths so the value can be sent to the Jupyter contents API.
|