xtralab 0.13.1 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/about/icons.d.ts +7 -0
- package/lib/about/icons.js +11 -0
- package/lib/about/index.d.ts +13 -0
- package/lib/about/index.js +81 -0
- package/lib/index.js +4 -0
- 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/omnibox/index.d.ts +5 -3
- package/lib/omnibox/index.js +17 -4
- package/lib/omnibox/model.d.ts +6 -0
- package/lib/omnibox/model.js +67 -12
- package/lib/omnibox/recents.d.ts +9 -2
- package/lib/omnibox/recents.js +40 -15
- package/lib/omnibox/widget.d.ts +6 -0
- package/lib/omnibox/widget.js +3 -2
- package/logo.svg +63 -0
- package/package.json +4 -2
- package/schema/about.json +24 -0
- package/schema/menu-bar.json +37 -0
- package/src/about/icons.ts +13 -0
- package/src/about/index.tsx +125 -0
- package/src/index.ts +4 -0
- package/src/menuBar/icons.ts +14 -0
- package/src/menuBar/index.ts +195 -0
- package/src/omnibox/index.ts +19 -4
- package/src/omnibox/model.ts +73 -11
- package/src/omnibox/recents.ts +53 -25
- package/src/omnibox/widget.tsx +9 -1
- package/src/svg.d.ts +4 -0
- package/style/about.css +66 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
package/README.md
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
/**
|
|
3
|
+
* The project logo mark — `logo.svg` at the repository root, the same file
|
|
4
|
+
* the README and the docs site use. The labextension build inlines it as a
|
|
5
|
+
* raw string, so the dialog always shows the current artwork.
|
|
6
|
+
*/
|
|
7
|
+
export declare const aboutLogoIcon: LabIcon;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
import logoSvgstr from '../../logo.svg';
|
|
3
|
+
/**
|
|
4
|
+
* The project logo mark — `logo.svg` at the repository root, the same file
|
|
5
|
+
* the README and the docs site use. The labextension build inlines it as a
|
|
6
|
+
* raw string, so the dialog always shows the current artwork.
|
|
7
|
+
*/
|
|
8
|
+
export const aboutLogoIcon = new LabIcon({
|
|
9
|
+
name: 'xtralab:about-logo',
|
|
10
|
+
svgstr: logoSvgstr
|
|
11
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
/**
|
|
3
|
+
* An "About xtralab" dialog replacing the stock "About JupyterLab" one.
|
|
4
|
+
*
|
|
5
|
+
* The upstream `@jupyterlab/help-extension:about` plugin is disabled via
|
|
6
|
+
* `jupyterlab.disabledExtensions` (package.json), which removes both its
|
|
7
|
+
* `help:about` command and its Help menu contribution; this plugin registers
|
|
8
|
+
* the same command id and restores the menu entry through its own schema.
|
|
9
|
+
* The dialog leads with the xtralab version and keeps the underlying
|
|
10
|
+
* JupyterLab version visible underneath.
|
|
11
|
+
*/
|
|
12
|
+
declare const plugin: JupyterFrontEndPlugin<void>;
|
|
13
|
+
export default plugin;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Dialog, ICommandPalette, showDialog } from '@jupyterlab/apputils';
|
|
2
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
3
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { aboutLogoIcon } from './icons';
|
|
6
|
+
const PLUGIN_ID = 'xtralab:about';
|
|
7
|
+
/**
|
|
8
|
+
* Upstream command id. Reusing it keeps the Help menu entry (contributed by
|
|
9
|
+
* `schema/about.json` at the same rank as upstream), the command palette
|
|
10
|
+
* muscle memory, and any external caller of `help:about` working.
|
|
11
|
+
*/
|
|
12
|
+
const CommandIDs = {
|
|
13
|
+
about: 'help:about'
|
|
14
|
+
};
|
|
15
|
+
const REPO_URL = 'https://github.com/jtpio/xtralab';
|
|
16
|
+
/**
|
|
17
|
+
* An "About xtralab" dialog replacing the stock "About JupyterLab" one.
|
|
18
|
+
*
|
|
19
|
+
* The upstream `@jupyterlab/help-extension:about` plugin is disabled via
|
|
20
|
+
* `jupyterlab.disabledExtensions` (package.json), which removes both its
|
|
21
|
+
* `help:about` command and its Help menu contribution; this plugin registers
|
|
22
|
+
* the same command id and restores the menu entry through its own schema.
|
|
23
|
+
* The dialog leads with the xtralab version and keeps the underlying
|
|
24
|
+
* JupyterLab version visible underneath.
|
|
25
|
+
*/
|
|
26
|
+
const plugin = {
|
|
27
|
+
id: PLUGIN_ID,
|
|
28
|
+
description: 'Show an About dialog with the xtralab and JupyterLab versions.',
|
|
29
|
+
autoStart: true,
|
|
30
|
+
requires: [ISettingRegistry],
|
|
31
|
+
optional: [ICommandPalette, ITranslator],
|
|
32
|
+
activate: (app, settingRegistry, palette, translator) => {
|
|
33
|
+
const { commands } = app;
|
|
34
|
+
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
|
|
35
|
+
commands.addCommand(CommandIDs.about, {
|
|
36
|
+
label: trans.__('About %1', 'xtralab'),
|
|
37
|
+
describedBy: {
|
|
38
|
+
args: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
execute: async () => {
|
|
44
|
+
// The settings payload carries the version of the installed
|
|
45
|
+
// labextension (read server-side from its package.json), which is
|
|
46
|
+
// the release actually running — the page config only knows the
|
|
47
|
+
// JupyterLab version. 'N/A' is jupyterlab_server's not-found value.
|
|
48
|
+
const version = await settingRegistry
|
|
49
|
+
.load(PLUGIN_ID)
|
|
50
|
+
.then(settings => settings.version === 'N/A' ? '' : settings.version)
|
|
51
|
+
.catch(() => '');
|
|
52
|
+
const title = (React.createElement("span", { className: "jp-xtralab-About-header" },
|
|
53
|
+
React.createElement(aboutLogoIcon.react, { className: "jp-xtralab-About-logo", height: "56px", width: "56px" }),
|
|
54
|
+
React.createElement("div", { className: "jp-xtralab-About-header-info" },
|
|
55
|
+
React.createElement("span", { className: "jp-xtralab-About-wordmark" }, "xtralab"),
|
|
56
|
+
version ? (React.createElement("span", { className: "jp-xtralab-About-version" }, trans.__('Version %1', version))) : null)));
|
|
57
|
+
const body = (React.createElement("div", { className: "jp-xtralab-About-body" },
|
|
58
|
+
React.createElement("span", { className: "jp-xtralab-About-basedOn" }, trans.__('Based on JupyterLab %1', app.version)),
|
|
59
|
+
React.createElement("span", { className: "jp-xtralab-About-externalLinks" },
|
|
60
|
+
React.createElement("a", { href: REPO_URL, target: "_blank", rel: "noopener noreferrer" }, trans.__('xtralab on GitHub'))),
|
|
61
|
+
React.createElement("span", { className: "jp-xtralab-About-copyright" }, trans.__('© %1 Jeremy Tuloup', 2026))));
|
|
62
|
+
return showDialog({
|
|
63
|
+
title,
|
|
64
|
+
body,
|
|
65
|
+
buttons: [
|
|
66
|
+
Dialog.cancelButton({
|
|
67
|
+
label: trans.__('Close')
|
|
68
|
+
})
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (palette) {
|
|
74
|
+
palette.addItem({
|
|
75
|
+
command: CommandIDs.about,
|
|
76
|
+
category: trans.__('Help')
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
export default plugin;
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import aboutPlugin from './about';
|
|
1
2
|
import agentSessionsPlugin from './agentSessions';
|
|
2
3
|
import askAgentPlugin from './askAgent';
|
|
3
4
|
import commandBarPlugin from './commandBar';
|
|
@@ -8,6 +9,7 @@ import editorIndentPlugin from './editorIndent';
|
|
|
8
9
|
import gitPlugins from './git';
|
|
9
10
|
import highlightPlugin from './highlight';
|
|
10
11
|
import launcherPlugins from './launcher';
|
|
12
|
+
import menuBarPlugin from './menuBar';
|
|
11
13
|
import menusPlugin from './menus';
|
|
12
14
|
import omniboxPlugin from './omnibox';
|
|
13
15
|
import searchReplacePlugin from './searchReplace';
|
|
@@ -27,6 +29,7 @@ import walkthroughPlugin from './walkthrough';
|
|
|
27
29
|
* they are spread in.
|
|
28
30
|
*/
|
|
29
31
|
const plugins = [
|
|
32
|
+
aboutPlugin,
|
|
30
33
|
agentSessionsPlugin,
|
|
31
34
|
askAgentPlugin,
|
|
32
35
|
commandBarPlugin,
|
|
@@ -37,6 +40,7 @@ const plugins = [
|
|
|
37
40
|
...gitPlugins,
|
|
38
41
|
highlightPlugin,
|
|
39
42
|
...launcherPlugins,
|
|
43
|
+
menuBarPlugin,
|
|
40
44
|
menusPlugin,
|
|
41
45
|
omniboxPlugin,
|
|
42
46
|
searchReplacePlugin,
|
|
@@ -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/omnibox/index.d.ts
CHANGED
|
@@ -3,9 +3,11 @@ import { IOmnibox } from './tokens';
|
|
|
3
3
|
/**
|
|
4
4
|
* The omnibox: a single launcher overlay that fuzzy-searches workspace files
|
|
5
5
|
* (via jupyterlab-quickopen's gitignore-aware endpoint) and JupyterLab
|
|
6
|
-
* commands
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* commands (the command palette's entries plus registry commands the palette
|
|
7
|
+
* doesn't list), and routes a typed prompt to one of the configured agents
|
|
8
|
+
* (running it in a fresh terminal through the launcher's
|
|
9
|
+
* `xtralab:start-agent:<id>` commands). It is opened by the top-bar command
|
|
10
|
+
* bar.
|
|
9
11
|
*
|
|
10
12
|
* Commands run and files opened through the overlay are remembered (persisted
|
|
11
13
|
* in the state database) and offered again at the top while the query is
|
package/lib/omnibox/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ICommandPalette } from '@jupyterlab/apputils';
|
|
|
2
2
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
3
3
|
import { IStateDB } from '@jupyterlab/statedb';
|
|
4
4
|
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
5
|
-
import { Widget } from '@lumino/widgets';
|
|
5
|
+
import { CommandPalette, Widget } from '@lumino/widgets';
|
|
6
6
|
import { IAgentRegistry } from '../launcher/tokens';
|
|
7
7
|
import { OmniboxRecents } from './recents';
|
|
8
8
|
import { IOmnibox, OMNIBOX_OPEN_COMMAND } from './tokens';
|
|
@@ -11,9 +11,11 @@ const PLUGIN_ID = 'xtralab:omnibox';
|
|
|
11
11
|
/**
|
|
12
12
|
* The omnibox: a single launcher overlay that fuzzy-searches workspace files
|
|
13
13
|
* (via jupyterlab-quickopen's gitignore-aware endpoint) and JupyterLab
|
|
14
|
-
* commands
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* commands (the command palette's entries plus registry commands the palette
|
|
15
|
+
* doesn't list), and routes a typed prompt to one of the configured agents
|
|
16
|
+
* (running it in a fresh terminal through the launcher's
|
|
17
|
+
* `xtralab:start-agent:<id>` commands). It is opened by the top-bar command
|
|
18
|
+
* bar.
|
|
17
19
|
*
|
|
18
20
|
* Commands run and files opened through the overlay are remembered (persisted
|
|
19
21
|
* in the state database) and offered again at the top while the query is
|
|
@@ -38,6 +40,16 @@ const plugin = {
|
|
|
38
40
|
const { commands, docRegistry } = app;
|
|
39
41
|
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
|
|
40
42
|
const placeholder = trans.__('Search files and commands, or ask an agent…');
|
|
43
|
+
// The palette's items carry per-item args (e.g. one "Use Theme: …" per
|
|
44
|
+
// theme) that a raw command-registry scan can't see, but `ICommandPalette`
|
|
45
|
+
// exposes no item list and the widget itself is unreachable through the
|
|
46
|
+
// shell (`LabShell.add` defers it until layout restore, after which the
|
|
47
|
+
// modal setting re-parents it out of the left area). The wrapper's
|
|
48
|
+
// `_palette` field is private in TypeScript only, so read it guarded.
|
|
49
|
+
const paletteItems = () => {
|
|
50
|
+
const widget = palette === null || palette === void 0 ? void 0 : palette._palette;
|
|
51
|
+
return widget instanceof CommandPalette ? [...widget.items] : [];
|
|
52
|
+
};
|
|
41
53
|
const recents = new OmniboxRecents({ state });
|
|
42
54
|
// Restore the persisted recents only after the configured cap is known:
|
|
43
55
|
// `restore` trims to `maxItems`, so restoring at the constructor default
|
|
@@ -77,6 +89,7 @@ const plugin = {
|
|
|
77
89
|
close();
|
|
78
90
|
const widget = new OmniboxWidget({
|
|
79
91
|
commands,
|
|
92
|
+
paletteItems: paletteItems(),
|
|
80
93
|
docRegistry,
|
|
81
94
|
agents: agentRegistry ? agentRegistry.agents : [],
|
|
82
95
|
placeholder,
|
package/lib/omnibox/model.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
|
2
2
|
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
3
3
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
4
4
|
import type { CommandRegistry } from '@lumino/commands';
|
|
5
|
+
import type { CommandPalette } from '@lumino/widgets';
|
|
5
6
|
import type { IAgent } from '../launcher/agents';
|
|
6
7
|
import type { OmniboxRecents } from './recents';
|
|
7
8
|
/**
|
|
@@ -53,6 +54,11 @@ interface IOmniboxSections {
|
|
|
53
54
|
interface IComputeOptions {
|
|
54
55
|
query: string;
|
|
55
56
|
commands: CommandRegistry;
|
|
57
|
+
/**
|
|
58
|
+
* The command palette's items, whose labels are computed with each item's
|
|
59
|
+
* args — the only form in which entries like "Use Theme: …" exist.
|
|
60
|
+
*/
|
|
61
|
+
paletteItems: ReadonlyArray<CommandPalette.IItem>;
|
|
56
62
|
docRegistry: DocumentRegistry;
|
|
57
63
|
agents: IAgent[];
|
|
58
64
|
files: string[];
|
package/lib/omnibox/model.js
CHANGED
|
@@ -51,11 +51,11 @@ export function computeSections(options) {
|
|
|
51
51
|
* Run a command and record the use on success, so failed commands never enter
|
|
52
52
|
* the recents list.
|
|
53
53
|
*/
|
|
54
|
-
function executeCommand(commands, recents, id) {
|
|
54
|
+
function executeCommand(commands, recents, id, args) {
|
|
55
55
|
void commands
|
|
56
|
-
.execute(id)
|
|
56
|
+
.execute(id, args)
|
|
57
57
|
.then(() => {
|
|
58
|
-
recents === null || recents === void 0 ? void 0 : recents.touch('command', id);
|
|
58
|
+
recents === null || recents === void 0 ? void 0 : recents.touch('command', id, args);
|
|
59
59
|
})
|
|
60
60
|
.catch(reason => {
|
|
61
61
|
console.error(`xtralab omnibox: command "${id}" failed`, reason);
|
|
@@ -80,16 +80,16 @@ function openFile(commands, recents, path) {
|
|
|
80
80
|
* accessors can assume a context — e.g. an active notebook — the omnibox
|
|
81
81
|
* doesn't provide).
|
|
82
82
|
*/
|
|
83
|
-
function commandDisplay(commands, id) {
|
|
83
|
+
function commandDisplay(commands, id, args) {
|
|
84
84
|
if (!commands.hasCommand(id)) {
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
87
|
try {
|
|
88
|
-
const label = commands.label(id);
|
|
89
|
-
if (!label || !commands.isVisible(id)) {
|
|
88
|
+
const label = commands.label(id, args);
|
|
89
|
+
if (!label || !commands.isVisible(id, args)) {
|
|
90
90
|
return null;
|
|
91
91
|
}
|
|
92
|
-
return { label, caption: commands.caption(id) };
|
|
92
|
+
return { label, caption: commands.caption(id, args) };
|
|
93
93
|
}
|
|
94
94
|
catch (_a) {
|
|
95
95
|
return null;
|
|
@@ -103,6 +103,7 @@ function commandDisplay(commands, id) {
|
|
|
103
103
|
* unavailable — file entries are shown as recorded).
|
|
104
104
|
*/
|
|
105
105
|
function buildRecentItems(options, mode) {
|
|
106
|
+
var _a;
|
|
106
107
|
const { commands, docRegistry, files, recents } = options;
|
|
107
108
|
if (!recents) {
|
|
108
109
|
return [];
|
|
@@ -112,18 +113,18 @@ function buildRecentItems(options, mode) {
|
|
|
112
113
|
const items = [];
|
|
113
114
|
for (const entry of recents.entries(kind)) {
|
|
114
115
|
if (entry.kind === 'command') {
|
|
115
|
-
const display = commandDisplay(commands, entry.id);
|
|
116
|
+
const display = commandDisplay(commands, entry.id, entry.args);
|
|
116
117
|
if (!display) {
|
|
117
118
|
continue;
|
|
118
119
|
}
|
|
119
120
|
items.push({
|
|
120
121
|
kind: 'command',
|
|
121
|
-
key: `recent:command:${entry.id}`,
|
|
122
|
+
key: `recent:command:${entry.id}:${JSON.stringify((_a = entry.args) !== null && _a !== void 0 ? _a : null)}`,
|
|
122
123
|
label: display.label,
|
|
123
124
|
matchIndices: [],
|
|
124
125
|
caption: display.caption || undefined,
|
|
125
126
|
execute: () => {
|
|
126
|
-
executeCommand(commands, recents, entry.id);
|
|
127
|
+
executeCommand(commands, recents, entry.id, entry.args);
|
|
127
128
|
}
|
|
128
129
|
});
|
|
129
130
|
}
|
|
@@ -145,11 +146,65 @@ function buildRecentItems(options, mode) {
|
|
|
145
146
|
}
|
|
146
147
|
return items;
|
|
147
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Match the query against the palette's items first — their labels carry each
|
|
151
|
+
* item's args (one "Use Theme: …" per theme, one row per font-size key), which
|
|
152
|
+
* a raw registry scan can never produce — then against registry commands the
|
|
153
|
+
* palette doesn't present. Palette-covered ids are skipped in the registry
|
|
154
|
+
* pass so a command never also surfaces under its argless label.
|
|
155
|
+
*/
|
|
148
156
|
function matchCommands(options, term) {
|
|
149
|
-
const { commands, recents } = options;
|
|
157
|
+
const { commands, paletteItems, recents } = options;
|
|
150
158
|
const query = term.toLowerCase();
|
|
151
159
|
const scored = [];
|
|
160
|
+
const inPalette = new Set();
|
|
161
|
+
const seenKeys = new Set();
|
|
162
|
+
for (const item of paletteItems) {
|
|
163
|
+
inPalette.add(item.command);
|
|
164
|
+
const { command: id, args } = item;
|
|
165
|
+
let label = '';
|
|
166
|
+
let visible = true;
|
|
167
|
+
let caption = '';
|
|
168
|
+
try {
|
|
169
|
+
// An item's accessors can throw if the command assumes a context (e.g.
|
|
170
|
+
// an active notebook) the omnibox doesn't provide; skip those.
|
|
171
|
+
label = item.label;
|
|
172
|
+
visible = item.isVisible;
|
|
173
|
+
caption = item.caption;
|
|
174
|
+
}
|
|
175
|
+
catch (_a) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (!label || !visible) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const key = `command:${id}:${JSON.stringify(args)}`;
|
|
182
|
+
if (seenKeys.has(key)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
seenKeys.add(key);
|
|
186
|
+
const match = StringExt.matchSumOfSquares(label.toLowerCase(), query);
|
|
187
|
+
if (!match) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
scored.push({
|
|
191
|
+
score: match.score,
|
|
192
|
+
item: {
|
|
193
|
+
kind: 'command',
|
|
194
|
+
key,
|
|
195
|
+
label,
|
|
196
|
+
matchIndices: match.indices,
|
|
197
|
+
caption: caption || undefined,
|
|
198
|
+
execute: () => {
|
|
199
|
+
executeCommand(commands, recents, id, args);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
152
204
|
for (const id of commands.listCommands()) {
|
|
205
|
+
if (inPalette.has(id)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
153
208
|
let label = '';
|
|
154
209
|
let visible = true;
|
|
155
210
|
let caption = '';
|
|
@@ -160,7 +215,7 @@ function matchCommands(options, term) {
|
|
|
160
215
|
visible = commands.isVisible(id);
|
|
161
216
|
caption = commands.caption(id);
|
|
162
217
|
}
|
|
163
|
-
catch (
|
|
218
|
+
catch (_b) {
|
|
164
219
|
continue;
|
|
165
220
|
}
|
|
166
221
|
if (!label || !visible) {
|
package/lib/omnibox/recents.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IStateDB } from '@jupyterlab/statedb';
|
|
2
|
+
import type { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
2
3
|
/**
|
|
3
4
|
* Kinds of omnibox results whose uses are recorded.
|
|
4
5
|
*/
|
|
@@ -12,6 +13,11 @@ interface IRecentEntry {
|
|
|
12
13
|
* The command id, or the workspace-relative file path.
|
|
13
14
|
*/
|
|
14
15
|
id: string;
|
|
16
|
+
/**
|
|
17
|
+
* Arguments the command ran with, for palette-style entries whose label
|
|
18
|
+
* and behavior depend on them (e.g. one "Use Theme: …" per theme).
|
|
19
|
+
*/
|
|
20
|
+
args?: ReadonlyPartialJSONObject;
|
|
15
21
|
}
|
|
16
22
|
/**
|
|
17
23
|
* A most-recently-used list of omnibox results, generic over the result kind
|
|
@@ -34,9 +40,10 @@ export declare class OmniboxRecents {
|
|
|
34
40
|
*/
|
|
35
41
|
entries(kind?: RecentKind): IRecentEntry[];
|
|
36
42
|
/**
|
|
37
|
-
* Record a use of `id
|
|
43
|
+
* Record a use of `id` (with the args it ran with, if any), moving it to
|
|
44
|
+
* the front of its kind's list.
|
|
38
45
|
*/
|
|
39
|
-
touch(kind: RecentKind, id: string): void;
|
|
46
|
+
touch(kind: RecentKind, id: string, args?: ReadonlyPartialJSONObject): void;
|
|
40
47
|
/**
|
|
41
48
|
* Load the persisted list. Entries recorded before the load completes stay
|
|
42
49
|
* at the front, ahead of the restored history.
|