xtralab 0.14.0 → 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 +4 -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/menuBar/icons.d.ts +6 -0
- package/lib/menuBar/icons.js +13 -0
- package/lib/menuBar/index.d.ts +15 -0
- package/lib/menuBar/index.js +164 -0
- 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/menu-bar.json +37 -0
- 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 +4 -0
- package/src/launcher/agents.ts +16 -3
- package/src/launcher/icons.ts +16 -4
- package/src/launcher/schemaDefaults.ts +43 -13
- package/src/menuBar/icons.ts +14 -0
- package/src/menuBar/index.ts +195 -0
- 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
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { ReactWidget, Toolbar } from '@jupyterlab/ui-components';
|
|
2
|
+
import { AccordionToolbar, PanelWithToolbar, ReactWidget, Toolbar } from '@jupyterlab/ui-components';
|
|
3
3
|
import { Signal } from '@lumino/signaling';
|
|
4
|
-
import { PanelLayout, Widget } from '@lumino/widgets';
|
|
4
|
+
import { AccordionPanel, PanelLayout, Widget } from '@lumino/widgets';
|
|
5
5
|
import { FileBrowserComponent } from './fileBrowser';
|
|
6
6
|
import { xtralabFileBrowserIcon } from './icons';
|
|
7
7
|
export const FILE_BROWSER_ID = 'xtralab:file-browser';
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
* Persisted by the movable-sections plugin, so it must stay stable.
|
|
10
|
+
*/
|
|
11
|
+
const TREE_SECTION_ID = 'xtralab-file-tree-section';
|
|
12
|
+
/**
|
|
13
|
+
* Context menu selectors hang off this class, so it sits on the section and
|
|
14
|
+
* keeps matching once the tree moves to another panel.
|
|
13
15
|
*/
|
|
14
16
|
const FILE_BROWSER_CSS_CLASS = 'jp-xtralab-FileBrowser';
|
|
17
|
+
const PANEL_CSS_CLASS = 'jp-xtralab-FileBrowserPanel';
|
|
15
18
|
const TOOLBAR_CSS_CLASS = 'jp-xtralab-FileBrowser-toolbar';
|
|
16
19
|
const CONTENT_CSS_CLASS = 'jp-xtralab-FileBrowser-content';
|
|
20
|
+
const ACCORDION_CSS_CLASS = 'jp-xtralab-FileBrowser-accordion';
|
|
21
|
+
const MOVABLE_SECTION_CLASS = 'jp-movable-section';
|
|
17
22
|
/**
|
|
18
|
-
* Lumino widget
|
|
19
|
-
*
|
|
20
|
-
*
|
|
23
|
+
* Lumino widget hosting the React-based `@pierre/trees` file browser. The tree
|
|
24
|
+
* and its toolbar form one movable "Files" section, and sections from other
|
|
25
|
+
* panels can be moved in below it.
|
|
21
26
|
*/
|
|
22
27
|
export class XtralabFileBrowser extends Widget {
|
|
23
28
|
constructor(options) {
|
|
@@ -31,26 +36,37 @@ export class XtralabFileBrowser extends Widget {
|
|
|
31
36
|
this._collapseAllRequested = new Signal(this);
|
|
32
37
|
this._fileFilterVisible = false;
|
|
33
38
|
this._fileFilterVisibleChanged = new Signal(this);
|
|
34
|
-
|
|
39
|
+
this._accordion = null;
|
|
40
|
+
this._contentChanged = new Signal(this);
|
|
41
|
+
this._sectionAdded = new Signal(this);
|
|
42
|
+
const layout = (this._panelLayout = this.layout = new PanelLayout());
|
|
35
43
|
this._contentsManager = options.contentsManager;
|
|
36
44
|
this._docManager = options.docManager;
|
|
37
45
|
this._onOpenFile = options.onOpenFile;
|
|
46
|
+
this._translator = options.translator;
|
|
38
47
|
this.id = FILE_BROWSER_ID;
|
|
39
48
|
this.title.icon = xtralabFileBrowserIcon;
|
|
40
49
|
this.title.caption = 'xtralab File Browser';
|
|
41
|
-
this.addClass(
|
|
50
|
+
this.addClass(PANEL_CSS_CLASS);
|
|
42
51
|
this._toolbar = new Toolbar();
|
|
43
52
|
this._toolbar.addClass(TOOLBAR_CSS_CLASS);
|
|
44
53
|
this._toolbar.node.setAttribute('aria-label', 'file browser');
|
|
45
|
-
layout.addWidget(this._toolbar);
|
|
46
54
|
this._content = new XtralabFileTreeContent({
|
|
47
55
|
contentsManager: this._contentsManager,
|
|
48
56
|
docManager: this._docManager,
|
|
49
57
|
onOpenFile: this._onOpenFile,
|
|
58
|
+
translator: this._translator,
|
|
50
59
|
browser: this
|
|
51
60
|
});
|
|
52
61
|
this._content.addClass(CONTENT_CSS_CLASS);
|
|
53
|
-
|
|
62
|
+
this._section = new PanelWithToolbar({ toolbar: this._toolbar });
|
|
63
|
+
this._section.id = TREE_SECTION_ID;
|
|
64
|
+
// Only an accordion host draws this; a plain `PanelLayout` shows no title.
|
|
65
|
+
this._section.title.label = 'Files';
|
|
66
|
+
this._section.addClass(FILE_BROWSER_CSS_CLASS);
|
|
67
|
+
this._section.addWidget(this._toolbar);
|
|
68
|
+
this._section.addWidget(this._content);
|
|
69
|
+
layout.addWidget(this._section);
|
|
54
70
|
}
|
|
55
71
|
get contentsManager() {
|
|
56
72
|
return this._contentsManager;
|
|
@@ -121,6 +137,129 @@ export class XtralabFileBrowser extends Widget {
|
|
|
121
137
|
toggleFileFilter() {
|
|
122
138
|
this.setFileFilterVisible(!this._fileFilterVisible);
|
|
123
139
|
}
|
|
140
|
+
get sectionNode() {
|
|
141
|
+
return this._section.node;
|
|
142
|
+
}
|
|
143
|
+
get isEmpty() {
|
|
144
|
+
return this._panelLayout.widgets.length === 0;
|
|
145
|
+
}
|
|
146
|
+
get contentChanged() {
|
|
147
|
+
return this._contentChanged;
|
|
148
|
+
}
|
|
149
|
+
get sectionAdded() {
|
|
150
|
+
return this._sectionAdded;
|
|
151
|
+
}
|
|
152
|
+
get accordionPanel() {
|
|
153
|
+
return this._accordion;
|
|
154
|
+
}
|
|
155
|
+
get sections() {
|
|
156
|
+
if (!this._accordion) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
return this._accordion.widgets.filter(w => w !== this._section);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The toolbar is the section's title node: it is the tree's header row at
|
|
163
|
+
* home, and an accordion host renders it inside the section title.
|
|
164
|
+
*/
|
|
165
|
+
getSections() {
|
|
166
|
+
return this._isSectionHome()
|
|
167
|
+
? [
|
|
168
|
+
{
|
|
169
|
+
id: TREE_SECTION_ID,
|
|
170
|
+
titleNode: this._toolbar.node,
|
|
171
|
+
widget: this._section
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
: [];
|
|
175
|
+
}
|
|
176
|
+
removeSectionById(sectionId) {
|
|
177
|
+
if (sectionId !== TREE_SECTION_ID || !this._isSectionHome()) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
this._section.parent = null;
|
|
181
|
+
if (this._accordion && this._accordion.widgets.length === 0) {
|
|
182
|
+
this._accordion.dispose();
|
|
183
|
+
this._accordion = null;
|
|
184
|
+
}
|
|
185
|
+
// Away from home the toolbar must not offer "Move to …" as well;
|
|
186
|
+
// `getSections` re-marks it on the way back.
|
|
187
|
+
this._toolbar.node.classList.remove(MOVABLE_SECTION_CLASS);
|
|
188
|
+
this._contentChanged.emit();
|
|
189
|
+
return this._section;
|
|
190
|
+
}
|
|
191
|
+
reinsertSection(widget) {
|
|
192
|
+
if (this._accordion) {
|
|
193
|
+
this._accordion.insertWidget(0, widget);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
this._panelLayout.addWidget(widget);
|
|
197
|
+
this._restoreToolbar();
|
|
198
|
+
}
|
|
199
|
+
this._contentChanged.emit();
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The move plugin reconciles persisted moves only against sections announced
|
|
203
|
+
* after `registerSource`, and this panel builds its section in the constructor.
|
|
204
|
+
*/
|
|
205
|
+
announceSections() {
|
|
206
|
+
for (const entry of this.getSections()) {
|
|
207
|
+
this._sectionAdded.emit(entry);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The first hosted section moves the tree into an accordion.
|
|
212
|
+
*/
|
|
213
|
+
addSection(widget) {
|
|
214
|
+
if (!this._accordion) {
|
|
215
|
+
const accordion = new AccordionPanel({
|
|
216
|
+
layout: AccordionToolbar.createLayout({})
|
|
217
|
+
});
|
|
218
|
+
accordion.addClass(ACCORDION_CSS_CLASS);
|
|
219
|
+
if (this._section.parent === this) {
|
|
220
|
+
accordion.addWidget(this._section);
|
|
221
|
+
}
|
|
222
|
+
accordion.addWidget(widget);
|
|
223
|
+
this._accordion = accordion;
|
|
224
|
+
this._panelLayout.addWidget(accordion);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
this._accordion.addWidget(widget);
|
|
228
|
+
}
|
|
229
|
+
this._contentChanged.emit();
|
|
230
|
+
}
|
|
231
|
+
removeSectionWidget(widget) {
|
|
232
|
+
if (!this._accordion || widget.parent !== this._accordion) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
widget.parent = null;
|
|
236
|
+
const remaining = this._accordion.widgets;
|
|
237
|
+
const treeOnly = remaining.length === 1 && remaining[0] === this._section;
|
|
238
|
+
if (treeOnly || remaining.length === 0) {
|
|
239
|
+
if (treeOnly) {
|
|
240
|
+
if (this._section.isHidden) {
|
|
241
|
+
this._section.show();
|
|
242
|
+
}
|
|
243
|
+
this._panelLayout.addWidget(this._section);
|
|
244
|
+
this._restoreToolbar();
|
|
245
|
+
}
|
|
246
|
+
this._accordion.dispose();
|
|
247
|
+
this._accordion = null;
|
|
248
|
+
}
|
|
249
|
+
this._contentChanged.emit();
|
|
250
|
+
}
|
|
251
|
+
_isSectionHome() {
|
|
252
|
+
const parent = this._section.parent;
|
|
253
|
+
return parent === this || (!!this._accordion && parent === this._accordion);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* An accordion host moves the toolbar node into the section title and leaves
|
|
257
|
+
* it orphaned on detach; re-parenting makes the layout attach it again.
|
|
258
|
+
*/
|
|
259
|
+
_restoreToolbar() {
|
|
260
|
+
this._toolbar.parent = null;
|
|
261
|
+
this._section.insertWidget(0, this._toolbar);
|
|
262
|
+
}
|
|
124
263
|
}
|
|
125
264
|
/**
|
|
126
265
|
* The React-rendered region of the file browser. Lives inside the host
|
|
@@ -132,6 +271,6 @@ class XtralabFileTreeContent extends ReactWidget {
|
|
|
132
271
|
this._options = options;
|
|
133
272
|
}
|
|
134
273
|
render() {
|
|
135
|
-
return (React.createElement(FileBrowserComponent, { contentsManager: this._options.contentsManager, docManager: this._options.docManager, onOpenFile: this._options.onOpenFile, widget: this._options.browser }));
|
|
274
|
+
return (React.createElement(FileBrowserComponent, { contentsManager: this._options.contentsManager, docManager: this._options.docManager, onOpenFile: this._options.onOpenFile, translator: this._options.translator, widget: this._options.browser }));
|
|
136
275
|
}
|
|
137
276
|
}
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import aboutPlugin from './about';
|
|
|
2
2
|
import agentSessionsPlugin from './agentSessions';
|
|
3
3
|
import askAgentPlugin from './askAgent';
|
|
4
4
|
import commandBarPlugin from './commandBar';
|
|
5
|
+
import customPanelPlugin from './customPanel';
|
|
5
6
|
import fileBrowserPlugin from './fileBrowser';
|
|
6
7
|
import fileTypeIconsPlugin from './fileTypeIcons';
|
|
7
8
|
import editorBreadcrumbsPlugin from './editorBreadcrumbs';
|
|
@@ -9,6 +10,7 @@ import editorIndentPlugin from './editorIndent';
|
|
|
9
10
|
import gitPlugins from './git';
|
|
10
11
|
import highlightPlugin from './highlight';
|
|
11
12
|
import launcherPlugins from './launcher';
|
|
13
|
+
import menuBarPlugin from './menuBar';
|
|
12
14
|
import menusPlugin from './menus';
|
|
13
15
|
import omniboxPlugin from './omnibox';
|
|
14
16
|
import searchReplacePlugin from './searchReplace';
|
|
@@ -32,6 +34,7 @@ const plugins = [
|
|
|
32
34
|
agentSessionsPlugin,
|
|
33
35
|
askAgentPlugin,
|
|
34
36
|
commandBarPlugin,
|
|
37
|
+
customPanelPlugin,
|
|
35
38
|
editorBreadcrumbsPlugin,
|
|
36
39
|
editorIndentPlugin,
|
|
37
40
|
fileBrowserPlugin,
|
|
@@ -39,6 +42,7 @@ const plugins = [
|
|
|
39
42
|
...gitPlugins,
|
|
40
43
|
highlightPlugin,
|
|
41
44
|
...launcherPlugins,
|
|
45
|
+
menuBarPlugin,
|
|
42
46
|
menusPlugin,
|
|
43
47
|
omniboxPlugin,
|
|
44
48
|
searchReplacePlugin,
|
package/lib/launcher/agents.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface IAgent {
|
|
|
23
23
|
* the user types one into the launcher's prompt box. The semantics:
|
|
24
24
|
*
|
|
25
25
|
* - `[]` → prompt is appended as a positional argument:
|
|
26
|
-
* `<command> 'PROMPT'`. (Used by claude, codex, vibe.)
|
|
26
|
+
* `<command> 'PROMPT'`. (Used by claude, codex, vibe, pi.)
|
|
27
27
|
* - `['-i']` / `['--prompt']` → the prompt is preceded by a flag:
|
|
28
28
|
* `<command> -i 'PROMPT'`. (Used by copilot, opencode.)
|
|
29
29
|
* - `undefined` → the agent does not accept an initial prompt. The
|
package/lib/launcher/agents.js
CHANGED
|
@@ -7,8 +7,9 @@ import { BUILTIN_AGENT_ICONS } from './icons';
|
|
|
7
7
|
* icons come from), but use the bare CLI names a user would type in a
|
|
8
8
|
* terminal rather than the ACP wrapper binaries that project spawns.
|
|
9
9
|
* Antigravity (Google's agent-first IDE/CLI, `agy`) is the Google agent.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Pi (https://pi.dev, `earendil-works/pi`) is a minimal extensible coding
|
|
11
|
+
* agent; its mark comes straight from its site. Anything not on the user's
|
|
12
|
+
* `$PATH` is filtered out at activation time, so the wider list is harmless.
|
|
12
13
|
*/
|
|
13
14
|
const DEFAULTS = [
|
|
14
15
|
{
|
|
@@ -100,6 +101,18 @@ const DEFAULTS = [
|
|
|
100
101
|
rank: 7,
|
|
101
102
|
requireAvailable: true,
|
|
102
103
|
promptArgs: []
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'pi',
|
|
107
|
+
label: 'Pi',
|
|
108
|
+
caption: 'Start Pi in a new terminal.',
|
|
109
|
+
command: 'pi',
|
|
110
|
+
icon: BUILTIN_AGENT_ICONS.pi,
|
|
111
|
+
rank: 8,
|
|
112
|
+
requireAvailable: true,
|
|
113
|
+
// `pi [messages...]` starts the interactive TUI and sends the messages
|
|
114
|
+
// as the first user prompt, so the positional form applies.
|
|
115
|
+
promptArgs: []
|
|
103
116
|
}
|
|
104
117
|
];
|
|
105
118
|
/**
|
package/lib/launcher/icons.js
CHANGED
|
@@ -5,11 +5,14 @@ import { LabIcon } from '@jupyterlab/ui-components';
|
|
|
5
5
|
* (`jupyter_ai_acp_client/static/*.svg`, BSD-3-Clause); Antigravity post-dates
|
|
6
6
|
* that set, so its mark is the official glyph from `@lobehub/icons-static-svg`
|
|
7
7
|
* (MIT) — we ship the brand-colored `antigravity-color` variant so Antigravity
|
|
8
|
-
* reads as a multi-color Google mark.
|
|
8
|
+
* reads as a multi-color Google mark. Pi's mark is the official `logo-auto.svg`
|
|
9
|
+
* from https://pi.dev (MIT, `earendil-works/pi`). Small adjustments are applied
|
|
9
10
|
* to play nicely with JupyterLab themes:
|
|
10
|
-
* - Monochrome marks (Codex, Copilot, Goose) use `currentColor` so they
|
|
11
|
+
* - Monochrome marks (Codex, Copilot, Goose, Pi) use `currentColor` so they
|
|
11
12
|
* inherit the surrounding launcher card's text color rather than
|
|
12
|
-
* hard-coding black, which disappears on the dark theme.
|
|
13
|
+
* hard-coding black, which disappears on the dark theme. For Pi this
|
|
14
|
+
* replaces the upstream `prefers-color-scheme` style block, which follows
|
|
15
|
+
* the OS scheme rather than the active JupyterLab theme.
|
|
13
16
|
* - The Goose mark upstream is rendered on a white rounded rect; we drop
|
|
14
17
|
* the rect so the silhouette can sit on the launcher card directly.
|
|
15
18
|
* - SVG ids that would collide if two copies of the same artwork ended up
|
|
@@ -169,6 +172,13 @@ const opencodeIcon = new LabIcon({
|
|
|
169
172
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z" fill="white"/>
|
|
170
173
|
</svg>`
|
|
171
174
|
});
|
|
175
|
+
const piIcon = new LabIcon({
|
|
176
|
+
name: 'xtralab:agent-pi',
|
|
177
|
+
svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" fill="currentColor">
|
|
178
|
+
<path fill-rule="evenodd" d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"/>
|
|
179
|
+
<path d="M517.36 400H634.72V634.72H517.36Z"/>
|
|
180
|
+
</svg>`
|
|
181
|
+
});
|
|
172
182
|
const neovimIcon = new LabIcon({
|
|
173
183
|
name: 'xtralab:editor-neovim',
|
|
174
184
|
svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#57A143">
|
|
@@ -195,7 +205,8 @@ export const BUILTIN_AGENT_ICONS = {
|
|
|
195
205
|
goose: gooseIcon,
|
|
196
206
|
kiro: kiroIcon,
|
|
197
207
|
'mistral-vibe': mistralVibeIcon,
|
|
198
|
-
opencode: opencodeIcon
|
|
208
|
+
opencode: opencodeIcon,
|
|
209
|
+
pi: piIcon
|
|
199
210
|
};
|
|
200
211
|
/**
|
|
201
212
|
* Built-in icons keyed by the editor's id, used to resolve the icon for a
|
|
@@ -4,14 +4,7 @@ import type { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const LAUNCHER_PLUGIN_ID = "xtralab:launcher";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the Settings Editor shows the shipped list.
|
|
10
|
-
*
|
|
11
|
-
* The schema declares `jupyter.lab.transform: true`, so the registry defers
|
|
12
|
-
* loading the plugin until a transform is registered: callers must invoke this
|
|
13
|
-
* before their first `settingRegistry.load(LAUNCHER_PLUGIN_ID)`. Idempotent, so
|
|
14
|
-
* the two plugins that load these settings can both call it regardless of
|
|
15
|
-
* activation order.
|
|
7
|
+
* Must run before the first `load(LAUNCHER_PLUGIN_ID)` (the schema sets
|
|
8
|
+
* `jupyter.lab.transform: true`); idempotent across callers.
|
|
16
9
|
*/
|
|
17
10
|
export declare function registerLauncherSchemaDefaults(settingRegistry: ISettingRegistry): void;
|
|
@@ -5,20 +5,38 @@ import { defaultEditorSettings } from './editors';
|
|
|
5
5
|
*/
|
|
6
6
|
export const LAUNCHER_PLUGIN_ID = 'xtralab:launcher';
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* re-registering (the registry rejects a duplicate transformer).
|
|
8
|
+
* Registries that already have the transform (duplicates are rejected).
|
|
10
9
|
*/
|
|
11
10
|
const registered = new WeakSet();
|
|
12
11
|
/**
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
* Merge sparse user entries over the built-in defaults, keyed by `id`.
|
|
13
|
+
*/
|
|
14
|
+
function mergeSettingsById(defaults, user) {
|
|
15
|
+
const entries = Array.isArray(user) ? user : [];
|
|
16
|
+
const overrideById = new Map(entries.map(entry => [entry.id, entry]));
|
|
17
|
+
const merged = defaults.map(base => {
|
|
18
|
+
const override = overrideById.get(base.id);
|
|
19
|
+
if (!override) {
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
22
|
+
overrideById.delete(base.id);
|
|
23
|
+
return { ...base, ...override };
|
|
24
|
+
});
|
|
25
|
+
merged.push(...overrideById.values());
|
|
26
|
+
// Item schema defaults are applied before this transform runs; without the
|
|
27
|
+
// refill, sparse entries render with unchecked boxes.
|
|
28
|
+
return merged.map(entry => {
|
|
29
|
+
var _a, _b;
|
|
30
|
+
return ({
|
|
31
|
+
...entry,
|
|
32
|
+
enabled: (_a = entry.enabled) !== null && _a !== void 0 ? _a : true,
|
|
33
|
+
requireAvailable: (_b = entry.requireAvailable) !== null && _b !== void 0 ? _b : true
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Must run before the first `load(LAUNCHER_PLUGIN_ID)` (the schema sets
|
|
39
|
+
* `jupyter.lab.transform: true`); idempotent across callers.
|
|
22
40
|
*/
|
|
23
41
|
export function registerLauncherSchemaDefaults(settingRegistry) {
|
|
24
42
|
if (registered.has(settingRegistry)) {
|
|
@@ -26,11 +44,17 @@ export function registerLauncherSchemaDefaults(settingRegistry) {
|
|
|
26
44
|
}
|
|
27
45
|
registered.add(settingRegistry);
|
|
28
46
|
settingRegistry.transform(LAUNCHER_PLUGIN_ID, {
|
|
47
|
+
compose: plugin => {
|
|
48
|
+
const { user } = plugin.data;
|
|
49
|
+
const composite = { ...plugin.data.composite };
|
|
50
|
+
composite.agents = mergeSettingsById(defaultAgentSettings(), user.agents);
|
|
51
|
+
composite.editors = mergeSettingsById(defaultEditorSettings(), user.editors);
|
|
52
|
+
plugin.data = { composite, user };
|
|
53
|
+
return plugin;
|
|
54
|
+
},
|
|
29
55
|
fetch: plugin => {
|
|
30
56
|
const properties = plugin.schema.properties;
|
|
31
57
|
if (properties) {
|
|
32
|
-
// The settings shapes are plain JSON by construction, but their named
|
|
33
|
-
// interfaces lack an index signature, so cast at this TS→JSON boundary.
|
|
34
58
|
if (properties.agents) {
|
|
35
59
|
properties.agents.default =
|
|
36
60
|
defaultAgentSettings();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
/**
|
|
3
|
+
* Hamburger mark for the collapsed main-menu button, matching the sidebar
|
|
4
|
+
* toggles in ../topBar/icons.ts.
|
|
5
|
+
*/
|
|
6
|
+
export const mainMenuIcon = new LabIcon({
|
|
7
|
+
name: 'xtralab:main-menu',
|
|
8
|
+
svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
|
9
|
+
<line x1="4.5" y1="7" x2="19.5" y2="7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
|
|
10
|
+
<line x1="4.5" y1="12" x2="19.5" y2="12" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
|
|
11
|
+
<line x1="4.5" y1="17" x2="19.5" y2="17" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
|
|
12
|
+
</svg>`
|
|
13
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
/**
|
|
3
|
+
* Collapse the main menu bar into a compact menu button.
|
|
4
|
+
*
|
|
5
|
+
* The menu bar is hidden and a hamburger button in the top bar opens the same
|
|
6
|
+
* menus as a vertical popup. The popup reuses the live `RankedMenu` instances
|
|
7
|
+
* owned by the `MainMenu`, so menus added or removed at runtime are reflected
|
|
8
|
+
* without duplication.
|
|
9
|
+
*
|
|
10
|
+
* Hiding covers the `jp-menu-panel` container too, which has its own
|
|
11
|
+
* `min-height` and would otherwise leave an empty strip. The "Show Menu Bar"
|
|
12
|
+
* toggle restores the classic bar.
|
|
13
|
+
*/
|
|
14
|
+
declare const plugin: JupyterFrontEndPlugin<void>;
|
|
15
|
+
export default plugin;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { ILabShell } from '@jupyterlab/application';
|
|
2
|
+
import { ICommandPalette } from '@jupyterlab/apputils';
|
|
3
|
+
import { IMainMenu, MainMenu } from '@jupyterlab/mainmenu';
|
|
4
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
5
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
6
|
+
import { CommandToolbarButton, MenuSvg } from '@jupyterlab/ui-components';
|
|
7
|
+
import { Menu } from '@lumino/widgets';
|
|
8
|
+
import { mainMenuIcon } from './icons';
|
|
9
|
+
const PLUGIN_ID = 'xtralab:menu-bar';
|
|
10
|
+
const OPEN_COMMAND = 'xtralab:open-main-menu';
|
|
11
|
+
const TOGGLE_COMMAND = 'xtralab:toggle-menu-bar';
|
|
12
|
+
/**
|
|
13
|
+
* How long after the popup closes a button press still counts as "close
|
|
14
|
+
* only". An open Lumino menu closes itself on any outside press from a
|
|
15
|
+
* document `pointerdown` listener in the capture phase, which runs before
|
|
16
|
+
* the button's own mousedown handler — so without this window, pressing the
|
|
17
|
+
* button while the popup is open would close it and instantly reopen it.
|
|
18
|
+
*/
|
|
19
|
+
const REOPEN_GUARD_MS = 250;
|
|
20
|
+
/**
|
|
21
|
+
* Collapse the main menu bar into a compact menu button.
|
|
22
|
+
*
|
|
23
|
+
* The menu bar is hidden and a hamburger button in the top bar opens the same
|
|
24
|
+
* menus as a vertical popup. The popup reuses the live `RankedMenu` instances
|
|
25
|
+
* owned by the `MainMenu`, so menus added or removed at runtime are reflected
|
|
26
|
+
* without duplication.
|
|
27
|
+
*
|
|
28
|
+
* Hiding covers the `jp-menu-panel` container too, which has its own
|
|
29
|
+
* `min-height` and would otherwise leave an empty strip. The "Show Menu Bar"
|
|
30
|
+
* toggle restores the classic bar.
|
|
31
|
+
*/
|
|
32
|
+
const plugin = {
|
|
33
|
+
id: PLUGIN_ID,
|
|
34
|
+
description: 'Collapse the main menu bar into a compact menu button.',
|
|
35
|
+
autoStart: true,
|
|
36
|
+
requires: [IMainMenu, ILabShell, ISettingRegistry],
|
|
37
|
+
optional: [ICommandPalette, ITranslator],
|
|
38
|
+
activate: (app, mainMenu, labShell, settingRegistry, palette, translator) => {
|
|
39
|
+
var _a;
|
|
40
|
+
const { commands } = app;
|
|
41
|
+
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
|
|
42
|
+
if (!(mainMenu instanceof MainMenu)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const menuPanel = () => {
|
|
46
|
+
var _a, _b;
|
|
47
|
+
return (_b = (_a = mainMenu.parent) !== null && _a !== void 0 ? _a : Array.from(labShell.widgets('top')).find(w => w.id === 'jp-menu-panel')) !== null && _b !== void 0 ? _b : null;
|
|
48
|
+
};
|
|
49
|
+
let visible = false;
|
|
50
|
+
let button = null;
|
|
51
|
+
let settings = null;
|
|
52
|
+
const applyVisibility = (value) => {
|
|
53
|
+
var _a;
|
|
54
|
+
visible = value;
|
|
55
|
+
mainMenu.setHidden(!value);
|
|
56
|
+
(_a = menuPanel()) === null || _a === void 0 ? void 0 : _a.setHidden(!value);
|
|
57
|
+
button === null || button === void 0 ? void 0 : button.setHidden(value);
|
|
58
|
+
commands.notifyCommandChanged(TOGGLE_COMMAND);
|
|
59
|
+
};
|
|
60
|
+
mainMenu.hide();
|
|
61
|
+
(_a = menuPanel()) === null || _a === void 0 ? void 0 : _a.hide();
|
|
62
|
+
let popup = null;
|
|
63
|
+
let popupClosedAt = 0;
|
|
64
|
+
const openMenu = () => {
|
|
65
|
+
var _a, _b;
|
|
66
|
+
if (visible) {
|
|
67
|
+
// The classic bar is on screen; behave like keyboard menu
|
|
68
|
+
// activation instead of opening a redundant popup that would
|
|
69
|
+
// compete with the bar for the same Menu instances.
|
|
70
|
+
mainMenu.activeIndex = 0;
|
|
71
|
+
mainMenu.openActiveMenu();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (Date.now() - popupClosedAt < REOPEN_GUARD_MS) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// One popup instance, rebuilt from the bar's current menus on every
|
|
78
|
+
// open so runtime menu changes stay reflected. It is a plain Lumino
|
|
79
|
+
// Menu rather than a MenuSvg: MenuSvg.insertItem re-wraps each
|
|
80
|
+
// submenu's renderer and insertItem on every call, which would pile
|
|
81
|
+
// wrappers onto the shared menus across reopens. The svg renderer and
|
|
82
|
+
// themed-container class are applied directly instead, matching how
|
|
83
|
+
// the real menus are constructed in MainMenu.
|
|
84
|
+
if (popup === null) {
|
|
85
|
+
popup = new Menu({ commands, renderer: MenuSvg.defaultRenderer });
|
|
86
|
+
popup.addClass('jp-ThemedContainer');
|
|
87
|
+
popup.addClass('jp-xtralab-MainMenuPopup');
|
|
88
|
+
popup.aboutToClose.connect(() => {
|
|
89
|
+
popupClosedAt = Date.now();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
popup.clearItems();
|
|
94
|
+
}
|
|
95
|
+
// Skip empty menus: MenuBar disables their headings, but submenu items
|
|
96
|
+
// have no such affordance and would open a blank panel.
|
|
97
|
+
for (const menu of mainMenu.menus) {
|
|
98
|
+
if (menu.items.length > 0) {
|
|
99
|
+
popup.addItem({ type: 'submenu', submenu: menu });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const anchor = button === null || button === void 0 ? void 0 : button.node.getBoundingClientRect();
|
|
103
|
+
// The 3px gap matches the detached-dropdown offset chrome.css gives
|
|
104
|
+
// the real menu bar's dropdowns (.lm-Menu.lm-MenuBar-menu).
|
|
105
|
+
popup.open((_a = anchor === null || anchor === void 0 ? void 0 : anchor.left) !== null && _a !== void 0 ? _a : 0, ((_b = anchor === null || anchor === void 0 ? void 0 : anchor.bottom) !== null && _b !== void 0 ? _b : 0) + 3);
|
|
106
|
+
};
|
|
107
|
+
commands.addCommand(OPEN_COMMAND, {
|
|
108
|
+
label: trans.__('Open Menu'),
|
|
109
|
+
caption: trans.__('Open the main menu'),
|
|
110
|
+
icon: mainMenuIcon,
|
|
111
|
+
execute: openMenu
|
|
112
|
+
});
|
|
113
|
+
commands.addCommand(TOGGLE_COMMAND, {
|
|
114
|
+
label: trans.__('Show Menu Bar'),
|
|
115
|
+
caption: trans.__('Toggle the visibility of the main menu bar'),
|
|
116
|
+
isToggled: () => visible,
|
|
117
|
+
execute: async () => {
|
|
118
|
+
applyVisibility(!visible);
|
|
119
|
+
if (settings !== null) {
|
|
120
|
+
try {
|
|
121
|
+
await settings.set('visible', visible);
|
|
122
|
+
}
|
|
123
|
+
catch (reason) {
|
|
124
|
+
console.error('xtralab: failed to persist menu bar visibility', reason);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
if (palette) {
|
|
130
|
+
const category = trans.__('Other');
|
|
131
|
+
palette.addItem({ command: OPEN_COMMAND, category });
|
|
132
|
+
palette.addItem({ command: TOGGLE_COMMAND, category });
|
|
133
|
+
}
|
|
134
|
+
Promise.all([settingRegistry.load(PLUGIN_ID), app.restored])
|
|
135
|
+
.then(([loaded]) => {
|
|
136
|
+
settings = loaded;
|
|
137
|
+
button = new CommandToolbarButton({
|
|
138
|
+
commands,
|
|
139
|
+
id: OPEN_COMMAND,
|
|
140
|
+
icon: mainMenuIcon,
|
|
141
|
+
label: '',
|
|
142
|
+
caption: trans.__('Menu'),
|
|
143
|
+
noFocusOnClick: true,
|
|
144
|
+
'aria-haspopup': 'menu'
|
|
145
|
+
});
|
|
146
|
+
button.id = 'xtralab-main-menu-button';
|
|
147
|
+
button.addClass('jp-xtralab-TopBarButton');
|
|
148
|
+
labShell.add(button, 'top', { rank: 0 });
|
|
149
|
+
const update = () => {
|
|
150
|
+
applyVisibility(loaded.get('visible').composite);
|
|
151
|
+
};
|
|
152
|
+
update();
|
|
153
|
+
loaded.changed.connect(update);
|
|
154
|
+
})
|
|
155
|
+
.catch(reason => {
|
|
156
|
+
// Without settings the collapsed state cannot be managed (and the
|
|
157
|
+
// button was never added) — reveal the stock bar rather than leave
|
|
158
|
+
// the app with no reachable menus.
|
|
159
|
+
applyVisibility(true);
|
|
160
|
+
console.error(`Failed to load settings for ${PLUGIN_ID}`, reason);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
export default plugin;
|
package/lib/sidebar/index.d.ts
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* The menu placement is contributed declaratively in
|
|
11
|
-
* `schema/sidebar.json`, so this plugin only registers commands and
|
|
12
|
-
* applies state — it does not touch `IMainMenu` directly.
|
|
13
|
-
*
|
|
14
|
-
* The preference is persisted to `xtralab:sidebar` settings so the choice
|
|
15
|
-
* survives reloads.
|
|
3
|
+
* Toggles individual sidebar tabs from View > Appearance > Sidebars,
|
|
4
|
+
* whichever side a tab currently lives on. Hiding detaches the widget
|
|
5
|
+
* (`widget.parent = null`) while keeping the instance for a later
|
|
6
|
+
* re-add; the preference persists in this plugin's settings. Which side
|
|
7
|
+
* a hidden tab came from is only tracked in-memory, so after a reload a
|
|
8
|
+
* re-shown tab lands on the left. The menu placement is contributed
|
|
9
|
+
* declaratively in `schema/sidebar.json`.
|
|
16
10
|
*/
|
|
17
11
|
declare const plugin: JupyterFrontEndPlugin<void>;
|
|
18
12
|
export default plugin;
|