xtralab 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +2 -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/src/about/icons.ts +13 -0
- package/src/about/index.tsx +125 -0
- package/src/index.ts +2 -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';
|
|
@@ -27,6 +28,7 @@ import walkthroughPlugin from './walkthrough';
|
|
|
27
28
|
* they are spread in.
|
|
28
29
|
*/
|
|
29
30
|
const plugins = [
|
|
31
|
+
aboutPlugin,
|
|
30
32
|
agentSessionsPlugin,
|
|
31
33
|
askAgentPlugin,
|
|
32
34
|
commandBarPlugin,
|
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.
|
package/lib/omnibox/recents.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of an entry: two uses only match when kind, id and args all do,
|
|
3
|
+
* so runs of one command with different args stay distinct entries.
|
|
4
|
+
*/
|
|
5
|
+
function entryKey(entry) {
|
|
6
|
+
var _a;
|
|
7
|
+
return `${entry.kind}\0${entry.id}\0${JSON.stringify((_a = entry.args) !== null && _a !== void 0 ? _a : null)}`;
|
|
8
|
+
}
|
|
1
9
|
/**
|
|
2
10
|
* State database key the recents list is persisted under.
|
|
3
11
|
*/
|
|
@@ -45,11 +53,14 @@ export class OmniboxRecents {
|
|
|
45
53
|
: [...this._entries];
|
|
46
54
|
}
|
|
47
55
|
/**
|
|
48
|
-
* Record a use of `id
|
|
56
|
+
* Record a use of `id` (with the args it ran with, if any), moving it to
|
|
57
|
+
* the front of its kind's list.
|
|
49
58
|
*/
|
|
50
|
-
touch(kind, id) {
|
|
51
|
-
const
|
|
52
|
-
|
|
59
|
+
touch(kind, id, args) {
|
|
60
|
+
const entry = args === undefined ? { kind, id } : { kind, id, args };
|
|
61
|
+
const key = entryKey(entry);
|
|
62
|
+
const rest = this._entries.filter(existing => entryKey(existing) !== key);
|
|
63
|
+
this._entries = this._trim([entry, ...rest]);
|
|
53
64
|
this._save();
|
|
54
65
|
}
|
|
55
66
|
/**
|
|
@@ -68,16 +79,31 @@ export class OmniboxRecents {
|
|
|
68
79
|
return;
|
|
69
80
|
}
|
|
70
81
|
const persisted = Array.isArray(fetched)
|
|
71
|
-
? fetched.filter((entry) =>
|
|
72
|
-
typeof entry
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
? fetched.filter((entry) => {
|
|
83
|
+
if (!entry || typeof entry !== 'object') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const candidate = entry;
|
|
87
|
+
if (candidate.kind !== 'command' && candidate.kind !== 'file') {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (typeof candidate.id !== 'string') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const args = candidate.args;
|
|
94
|
+
return (args === undefined ||
|
|
95
|
+
(typeof args === 'object' && args !== null && !Array.isArray(args)));
|
|
96
|
+
})
|
|
76
97
|
: [];
|
|
98
|
+
const known = new Set(this._entries.map(entryKey));
|
|
77
99
|
const merged = [...this._entries];
|
|
78
100
|
for (const entry of persisted) {
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
const key = entryKey(entry);
|
|
102
|
+
if (!known.has(key)) {
|
|
103
|
+
known.add(key);
|
|
104
|
+
merged.push(entry.args === undefined
|
|
105
|
+
? { kind: entry.kind, id: entry.id }
|
|
106
|
+
: { kind: entry.kind, id: entry.id, args: entry.args });
|
|
81
107
|
}
|
|
82
108
|
}
|
|
83
109
|
this._entries = this._trim(merged);
|
|
@@ -98,10 +124,9 @@ export class OmniboxRecents {
|
|
|
98
124
|
if (!this._state) {
|
|
99
125
|
return;
|
|
100
126
|
}
|
|
101
|
-
const value = this._entries.map(entry =>
|
|
102
|
-
kind: entry.kind,
|
|
103
|
-
id: entry.id
|
|
104
|
-
}));
|
|
127
|
+
const value = this._entries.map(entry => entry.args === undefined
|
|
128
|
+
? { kind: entry.kind, id: entry.id }
|
|
129
|
+
: { kind: entry.kind, id: entry.id, args: entry.args });
|
|
105
130
|
this._state.save(STATE_KEY, value).catch(reason => {
|
|
106
131
|
console.error('xtralab omnibox: failed to save recents', reason);
|
|
107
132
|
});
|
package/lib/omnibox/widget.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
|
2
2
|
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
3
3
|
import { ReactWidget } 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
|
/**
|
|
@@ -24,6 +25,11 @@ export declare namespace OmniboxWidget {
|
|
|
24
25
|
*/
|
|
25
26
|
interface IOptions {
|
|
26
27
|
commands: CommandRegistry;
|
|
28
|
+
/**
|
|
29
|
+
* Snapshot of the command palette's items, read once when the overlay
|
|
30
|
+
* opens; empty when the palette isn't available.
|
|
31
|
+
*/
|
|
32
|
+
paletteItems: ReadonlyArray<CommandPalette.IItem>;
|
|
27
33
|
docRegistry: DocumentRegistry;
|
|
28
34
|
/**
|
|
29
35
|
* Snapshot of the available agents, read once when the overlay opens.
|
package/lib/omnibox/widget.js
CHANGED
|
@@ -39,7 +39,7 @@ function renderIcon(icon) {
|
|
|
39
39
|
return icon ? (React.createElement(icon.react, { tag: "span", className: "jp-xtralab-Omnibox-itemIcon" })) : (React.createElement("span", { className: "jp-xtralab-Omnibox-itemIcon" }));
|
|
40
40
|
}
|
|
41
41
|
function OmniboxComponent(props) {
|
|
42
|
-
const { commands, docRegistry, agents, placeholder, initialQuery, recents, trans, onClose } = props;
|
|
42
|
+
const { commands, paletteItems, docRegistry, agents, placeholder, initialQuery, recents, trans, onClose } = props;
|
|
43
43
|
const [query, setQuery] = React.useState(initialQuery);
|
|
44
44
|
const [files, setFiles] = React.useState([]);
|
|
45
45
|
const [active, setActive] = React.useState(0);
|
|
@@ -63,12 +63,13 @@ function OmniboxComponent(props) {
|
|
|
63
63
|
const sections = React.useMemo(() => computeSections({
|
|
64
64
|
query,
|
|
65
65
|
commands,
|
|
66
|
+
paletteItems,
|
|
66
67
|
docRegistry,
|
|
67
68
|
agents,
|
|
68
69
|
files,
|
|
69
70
|
recents,
|
|
70
71
|
trans
|
|
71
|
-
}), [query, commands, docRegistry, agents, files, recents, trans]);
|
|
72
|
+
}), [query, commands, paletteItems, docRegistry, agents, files, recents, trans]);
|
|
72
73
|
const flat = React.useMemo(() => [
|
|
73
74
|
...sections.recent,
|
|
74
75
|
...sections.commands,
|
package/logo.svg
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -3 194 272" role="img" aria-labelledby="jp-xtralab-logo-title jp-xtralab-logo-desc">
|
|
2
|
+
<title id="jp-xtralab-logo-title">XtraLab flask icon</title>
|
|
3
|
+
<desc id="jp-xtralab-logo-desc">A clean, slightly tilted laboratory flask with a symmetrical constant-width dark frame, blue-to-pink liquid, bubbles, and rising pixel blocks.</desc>
|
|
4
|
+
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id="jp-xtralab-logo-liquid" x1="20" y1="154" x2="151" y2="231" gradientUnits="userSpaceOnUse">
|
|
7
|
+
<stop offset="0" stop-color="#00a6ed"/>
|
|
8
|
+
<stop offset=".29" stop-color="#0879ed"/>
|
|
9
|
+
<stop offset=".58" stop-color="#6548d2"/>
|
|
10
|
+
<stop offset=".8" stop-color="#b43baa"/>
|
|
11
|
+
<stop offset="1" stop-color="#f1357e"/>
|
|
12
|
+
</linearGradient>
|
|
13
|
+
<radialGradient id="jp-xtralab-logo-liquid-glow" cx="0" cy="0" r="1"
|
|
14
|
+
gradientTransform="translate(49 169) rotate(34) scale(75 49)"
|
|
15
|
+
gradientUnits="userSpaceOnUse">
|
|
16
|
+
<stop stop-color="#18b9ef" stop-opacity=".78"/>
|
|
17
|
+
<stop offset=".58" stop-color="#1688ef" stop-opacity=".2"/>
|
|
18
|
+
<stop offset="1" stop-color="#1688ef" stop-opacity="0"/>
|
|
19
|
+
</radialGradient>
|
|
20
|
+
<linearGradient id="jp-xtralab-logo-pixel-blue" x1="137" y1="13" x2="156" y2="32" gradientUnits="userSpaceOnUse">
|
|
21
|
+
<stop stop-color="#05aae9"/>
|
|
22
|
+
<stop offset="1" stop-color="#008be0"/>
|
|
23
|
+
</linearGradient>
|
|
24
|
+
<linearGradient id="jp-xtralab-logo-pixel-purple" x1="154" y1="39" x2="178" y2="61" gradientUnits="userSpaceOnUse">
|
|
25
|
+
<stop stop-color="#6242c0"/>
|
|
26
|
+
<stop offset="1" stop-color="#923ba8"/>
|
|
27
|
+
</linearGradient>
|
|
28
|
+
<linearGradient id="jp-xtralab-logo-pixel-pink" x1="166" y1="0" x2="183" y2="17" gradientUnits="userSpaceOnUse">
|
|
29
|
+
<stop stop-color="#8040bd"/>
|
|
30
|
+
<stop offset="1" stop-color="#ad3c9b"/>
|
|
31
|
+
</linearGradient>
|
|
32
|
+
<clipPath id="jp-xtralab-logo-chamber" clipPathUnits="userSpaceOnUse">
|
|
33
|
+
<path d="M64 61h36v43c0 10 2 18 8 27l42 73c5 8 5 18 0 26-5 8-13 12-23 13H37c-10-1-18-5-23-13-5-8-5-18 0-26l42-73c6-9 8-17 8-27Z"/>
|
|
34
|
+
</clipPath>
|
|
35
|
+
</defs>
|
|
36
|
+
|
|
37
|
+
<!-- The flask is constructed symmetrically around x=82, then given a slight logo-matching tilt. -->
|
|
38
|
+
<g transform="rotate(9 82 232)">
|
|
39
|
+
<g clip-path="url(#jp-xtralab-logo-chamber)">
|
|
40
|
+
<path fill="url(#jp-xtralab-logo-liquid)"
|
|
41
|
+
d="M8 207c17-25 34-52 55-60 16-6 29-2 42 5 14 8 27 8 43-4l13 65c4 18-8 35-27 37H30c-20-2-30-23-22-43Z"/>
|
|
42
|
+
<path fill="url(#jp-xtralab-logo-liquid-glow)"
|
|
43
|
+
d="M8 207c17-25 34-52 55-60 16-6 29-2 42 5 14 8 27 8 43-4l13 65c4 18-8 35-27 37H30c-20-2-30-23-22-43Z"/>
|
|
44
|
+
</g>
|
|
45
|
+
|
|
46
|
+
<!-- A straight integrated mouth joins the widened chimney without a separate rim element. -->
|
|
47
|
+
<path d="M56 55v49c0 12-2 21-8 31L9 201c-7 10-7 23-1 33s16 15 28 16h92c12-1 22-6 28-16s6-23-1-33l-39-66c-6-10-8-19-8-31V55H56Z"
|
|
48
|
+
fill="none" stroke="#07111d" stroke-width="16" stroke-linejoin="round" stroke-linecap="round"/>
|
|
49
|
+
<path d="M56 55v49c0 12-2 21-8 31L9 201c-7 10-7 23-1 33s16 15 28 16h92c12-1 22-6 28-16s6-23-1-33l-39-66c-6-10-8-19-8-31V55H56Z"
|
|
50
|
+
fill="none" stroke="#263b57" stroke-width="12" stroke-linejoin="round" stroke-linecap="round"/>
|
|
51
|
+
|
|
52
|
+
<circle cx="82" cy="80" r="4.7" fill="#009fe9" stroke="#0b1d35" stroke-width=".7"/>
|
|
53
|
+
<circle cx="82" cy="104" r="6.2" fill="#078ae9" stroke="#0b1d35" stroke-width=".7"/>
|
|
54
|
+
<circle cx="82" cy="128" r="7.2" fill="#3256d9" stroke="#0b1d35" stroke-width=".7"/>
|
|
55
|
+
</g>
|
|
56
|
+
|
|
57
|
+
<g stroke="#17233c" stroke-width=".75">
|
|
58
|
+
<rect x="116" y="22" width="13" height="13" rx="2" fill="#7342bd"/>
|
|
59
|
+
<rect x="137" y="13" width="19" height="19" rx="1.5" fill="url(#jp-xtralab-logo-pixel-blue)"/>
|
|
60
|
+
<rect x="154" y="39" width="24" height="22" rx="1.5" fill="url(#jp-xtralab-logo-pixel-purple)"/>
|
|
61
|
+
<rect x="166" y=".5" width="17" height="17" rx="1.5" fill="url(#jp-xtralab-logo-pixel-pink)"/>
|
|
62
|
+
</g>
|
|
63
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtralab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
|
|
22
22
|
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
|
|
23
23
|
"src/**/*.{ts,tsx}",
|
|
24
|
-
"schema/*.json"
|
|
24
|
+
"schema/*.json",
|
|
25
|
+
"logo.svg"
|
|
25
26
|
],
|
|
26
27
|
"main": "lib/index.js",
|
|
27
28
|
"types": "lib/index.d.ts",
|
|
@@ -141,6 +142,7 @@
|
|
|
141
142
|
"@jupyterlab/git:notebook-diff",
|
|
142
143
|
"@jupyterlab/git:plain-text-diff",
|
|
143
144
|
"@jupyterlab/git:image-diff",
|
|
145
|
+
"@jupyterlab/help-extension:about",
|
|
144
146
|
"@jupyterlab/launcher-extension"
|
|
145
147
|
]
|
|
146
148
|
},
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"jupyter.lab.menus": {
|
|
3
|
+
"main": [
|
|
4
|
+
{
|
|
5
|
+
"id": "jp-mainmenu-help",
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"command": "help:about",
|
|
9
|
+
"rank": 0
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "separator",
|
|
13
|
+
"rank": 0
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"title": "xtralab About",
|
|
20
|
+
"description": "The About dialog showing the running xtralab and JupyterLab versions.",
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {},
|
|
23
|
+
"additionalProperties": false
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
|
|
3
|
+
import logoSvgstr from '../../logo.svg';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The project logo mark — `logo.svg` at the repository root, the same file
|
|
7
|
+
* the README and the docs site use. The labextension build inlines it as a
|
|
8
|
+
* raw string, so the dialog always shows the current artwork.
|
|
9
|
+
*/
|
|
10
|
+
export const aboutLogoIcon = new LabIcon({
|
|
11
|
+
name: 'xtralab:about-logo',
|
|
12
|
+
svgstr: logoSvgstr
|
|
13
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
import { Dialog, ICommandPalette, showDialog } from '@jupyterlab/apputils';
|
|
6
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
7
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
|
|
10
|
+
import { aboutLogoIcon } from './icons';
|
|
11
|
+
|
|
12
|
+
const PLUGIN_ID = 'xtralab:about';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Upstream command id. Reusing it keeps the Help menu entry (contributed by
|
|
16
|
+
* `schema/about.json` at the same rank as upstream), the command palette
|
|
17
|
+
* muscle memory, and any external caller of `help:about` working.
|
|
18
|
+
*/
|
|
19
|
+
const CommandIDs = {
|
|
20
|
+
about: 'help:about'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const REPO_URL = 'https://github.com/jtpio/xtralab';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An "About xtralab" dialog replacing the stock "About JupyterLab" one.
|
|
27
|
+
*
|
|
28
|
+
* The upstream `@jupyterlab/help-extension:about` plugin is disabled via
|
|
29
|
+
* `jupyterlab.disabledExtensions` (package.json), which removes both its
|
|
30
|
+
* `help:about` command and its Help menu contribution; this plugin registers
|
|
31
|
+
* the same command id and restores the menu entry through its own schema.
|
|
32
|
+
* The dialog leads with the xtralab version and keeps the underlying
|
|
33
|
+
* JupyterLab version visible underneath.
|
|
34
|
+
*/
|
|
35
|
+
const plugin: JupyterFrontEndPlugin<void> = {
|
|
36
|
+
id: PLUGIN_ID,
|
|
37
|
+
description: 'Show an About dialog with the xtralab and JupyterLab versions.',
|
|
38
|
+
autoStart: true,
|
|
39
|
+
requires: [ISettingRegistry],
|
|
40
|
+
optional: [ICommandPalette, ITranslator],
|
|
41
|
+
activate: (
|
|
42
|
+
app: JupyterFrontEnd,
|
|
43
|
+
settingRegistry: ISettingRegistry,
|
|
44
|
+
palette: ICommandPalette | null,
|
|
45
|
+
translator: ITranslator | null
|
|
46
|
+
): void => {
|
|
47
|
+
const { commands } = app;
|
|
48
|
+
const trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
49
|
+
|
|
50
|
+
commands.addCommand(CommandIDs.about, {
|
|
51
|
+
label: trans.__('About %1', 'xtralab'),
|
|
52
|
+
describedBy: {
|
|
53
|
+
args: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
execute: async () => {
|
|
59
|
+
// The settings payload carries the version of the installed
|
|
60
|
+
// labextension (read server-side from its package.json), which is
|
|
61
|
+
// the release actually running — the page config only knows the
|
|
62
|
+
// JupyterLab version. 'N/A' is jupyterlab_server's not-found value.
|
|
63
|
+
const version = await settingRegistry
|
|
64
|
+
.load(PLUGIN_ID)
|
|
65
|
+
.then(settings =>
|
|
66
|
+
settings.version === 'N/A' ? '' : settings.version
|
|
67
|
+
)
|
|
68
|
+
.catch(() => '');
|
|
69
|
+
|
|
70
|
+
const title = (
|
|
71
|
+
<span className="jp-xtralab-About-header">
|
|
72
|
+
<aboutLogoIcon.react
|
|
73
|
+
className="jp-xtralab-About-logo"
|
|
74
|
+
height="56px"
|
|
75
|
+
width="56px"
|
|
76
|
+
/>
|
|
77
|
+
<div className="jp-xtralab-About-header-info">
|
|
78
|
+
<span className="jp-xtralab-About-wordmark">xtralab</span>
|
|
79
|
+
{version ? (
|
|
80
|
+
<span className="jp-xtralab-About-version">
|
|
81
|
+
{trans.__('Version %1', version)}
|
|
82
|
+
</span>
|
|
83
|
+
) : null}
|
|
84
|
+
</div>
|
|
85
|
+
</span>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const body = (
|
|
89
|
+
<div className="jp-xtralab-About-body">
|
|
90
|
+
<span className="jp-xtralab-About-basedOn">
|
|
91
|
+
{trans.__('Based on JupyterLab %1', app.version)}
|
|
92
|
+
</span>
|
|
93
|
+
<span className="jp-xtralab-About-externalLinks">
|
|
94
|
+
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
|
95
|
+
{trans.__('xtralab on GitHub')}
|
|
96
|
+
</a>
|
|
97
|
+
</span>
|
|
98
|
+
<span className="jp-xtralab-About-copyright">
|
|
99
|
+
{trans.__('© %1 Jeremy Tuloup', 2026)}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return showDialog({
|
|
105
|
+
title,
|
|
106
|
+
body,
|
|
107
|
+
buttons: [
|
|
108
|
+
Dialog.cancelButton({
|
|
109
|
+
label: trans.__('Close')
|
|
110
|
+
})
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (palette) {
|
|
117
|
+
palette.addItem({
|
|
118
|
+
command: CommandIDs.about,
|
|
119
|
+
category: trans.__('Help')
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default plugin;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
2
|
|
|
3
|
+
import aboutPlugin from './about';
|
|
3
4
|
import agentSessionsPlugin from './agentSessions';
|
|
4
5
|
import askAgentPlugin from './askAgent';
|
|
5
6
|
import commandBarPlugin from './commandBar';
|
|
@@ -30,6 +31,7 @@ import walkthroughPlugin from './walkthrough';
|
|
|
30
31
|
* they are spread in.
|
|
31
32
|
*/
|
|
32
33
|
const plugins: JupyterFrontEndPlugin<unknown>[] = [
|
|
34
|
+
aboutPlugin,
|
|
33
35
|
agentSessionsPlugin,
|
|
34
36
|
askAgentPlugin,
|
|
35
37
|
commandBarPlugin,
|
package/src/omnibox/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { ICommandPalette } from '@jupyterlab/apputils';
|
|
|
6
6
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
7
7
|
import { IStateDB } from '@jupyterlab/statedb';
|
|
8
8
|
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
9
|
-
import { Widget } from '@lumino/widgets';
|
|
9
|
+
import { CommandPalette, Widget } from '@lumino/widgets';
|
|
10
10
|
|
|
11
11
|
import { IAgentRegistry } from '../launcher/tokens';
|
|
12
12
|
|
|
@@ -19,9 +19,11 @@ const PLUGIN_ID = 'xtralab:omnibox';
|
|
|
19
19
|
/**
|
|
20
20
|
* The omnibox: a single launcher overlay that fuzzy-searches workspace files
|
|
21
21
|
* (via jupyterlab-quickopen's gitignore-aware endpoint) and JupyterLab
|
|
22
|
-
* commands
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* commands (the command palette's entries plus registry commands the palette
|
|
23
|
+
* doesn't list), and routes a typed prompt to one of the configured agents
|
|
24
|
+
* (running it in a fresh terminal through the launcher's
|
|
25
|
+
* `xtralab:start-agent:<id>` commands). It is opened by the top-bar command
|
|
26
|
+
* bar.
|
|
25
27
|
*
|
|
26
28
|
* Commands run and files opened through the overlay are remembered (persisted
|
|
27
29
|
* in the state database) and offered again at the top while the query is
|
|
@@ -55,6 +57,18 @@ const plugin: JupyterFrontEndPlugin<IOmnibox> = {
|
|
|
55
57
|
const trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
56
58
|
const placeholder = trans.__('Search files and commands, or ask an agent…');
|
|
57
59
|
|
|
60
|
+
// The palette's items carry per-item args (e.g. one "Use Theme: …" per
|
|
61
|
+
// theme) that a raw command-registry scan can't see, but `ICommandPalette`
|
|
62
|
+
// exposes no item list and the widget itself is unreachable through the
|
|
63
|
+
// shell (`LabShell.add` defers it until layout restore, after which the
|
|
64
|
+
// modal setting re-parents it out of the left area). The wrapper's
|
|
65
|
+
// `_palette` field is private in TypeScript only, so read it guarded.
|
|
66
|
+
const paletteItems = (): ReadonlyArray<CommandPalette.IItem> => {
|
|
67
|
+
const widget = (palette as unknown as { _palette?: unknown } | null)
|
|
68
|
+
?._palette;
|
|
69
|
+
return widget instanceof CommandPalette ? [...widget.items] : [];
|
|
70
|
+
};
|
|
71
|
+
|
|
58
72
|
const recents = new OmniboxRecents({ state });
|
|
59
73
|
|
|
60
74
|
// Restore the persisted recents only after the configured cap is known:
|
|
@@ -100,6 +114,7 @@ const plugin: JupyterFrontEndPlugin<IOmnibox> = {
|
|
|
100
114
|
close();
|
|
101
115
|
const widget = new OmniboxWidget({
|
|
102
116
|
commands,
|
|
117
|
+
paletteItems: paletteItems(),
|
|
103
118
|
docRegistry,
|
|
104
119
|
agents: agentRegistry ? agentRegistry.agents : [],
|
|
105
120
|
placeholder,
|
package/src/omnibox/model.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { TranslationBundle } from '@jupyterlab/translation';
|
|
|
3
3
|
import { fileIcon, LabIcon } from '@jupyterlab/ui-components';
|
|
4
4
|
import { StringExt } from '@lumino/algorithm';
|
|
5
5
|
import type { CommandRegistry } from '@lumino/commands';
|
|
6
|
+
import type { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
7
|
+
import type { CommandPalette } from '@lumino/widgets';
|
|
6
8
|
|
|
7
9
|
import type { IAgent } from '../launcher/agents';
|
|
8
10
|
import { agentCommandId } from '../launcher/tokens';
|
|
@@ -61,6 +63,11 @@ interface IOmniboxSections {
|
|
|
61
63
|
interface IComputeOptions {
|
|
62
64
|
query: string;
|
|
63
65
|
commands: CommandRegistry;
|
|
66
|
+
/**
|
|
67
|
+
* The command palette's items, whose labels are computed with each item's
|
|
68
|
+
* args — the only form in which entries like "Use Theme: …" exist.
|
|
69
|
+
*/
|
|
70
|
+
paletteItems: ReadonlyArray<CommandPalette.IItem>;
|
|
64
71
|
docRegistry: DocumentRegistry;
|
|
65
72
|
agents: IAgent[];
|
|
66
73
|
files: string[];
|
|
@@ -131,12 +138,13 @@ export function computeSections(options: IComputeOptions): IOmniboxSections {
|
|
|
131
138
|
function executeCommand(
|
|
132
139
|
commands: CommandRegistry,
|
|
133
140
|
recents: OmniboxRecents | null,
|
|
134
|
-
id: string
|
|
141
|
+
id: string,
|
|
142
|
+
args?: ReadonlyPartialJSONObject
|
|
135
143
|
): void {
|
|
136
144
|
void commands
|
|
137
|
-
.execute(id)
|
|
145
|
+
.execute(id, args)
|
|
138
146
|
.then(() => {
|
|
139
|
-
recents?.touch('command', id);
|
|
147
|
+
recents?.touch('command', id, args);
|
|
140
148
|
})
|
|
141
149
|
.catch(reason => {
|
|
142
150
|
console.error(`xtralab omnibox: command "${id}" failed`, reason);
|
|
@@ -169,17 +177,18 @@ function openFile(
|
|
|
169
177
|
*/
|
|
170
178
|
function commandDisplay(
|
|
171
179
|
commands: CommandRegistry,
|
|
172
|
-
id: string
|
|
180
|
+
id: string,
|
|
181
|
+
args?: ReadonlyPartialJSONObject
|
|
173
182
|
): { label: string; caption: string } | null {
|
|
174
183
|
if (!commands.hasCommand(id)) {
|
|
175
184
|
return null;
|
|
176
185
|
}
|
|
177
186
|
try {
|
|
178
|
-
const label = commands.label(id);
|
|
179
|
-
if (!label || !commands.isVisible(id)) {
|
|
187
|
+
const label = commands.label(id, args);
|
|
188
|
+
if (!label || !commands.isVisible(id, args)) {
|
|
180
189
|
return null;
|
|
181
190
|
}
|
|
182
|
-
return { label, caption: commands.caption(id) };
|
|
191
|
+
return { label, caption: commands.caption(id, args) };
|
|
183
192
|
} catch {
|
|
184
193
|
return null;
|
|
185
194
|
}
|
|
@@ -207,18 +216,18 @@ function buildRecentItems(
|
|
|
207
216
|
const items: IOmniboxItem[] = [];
|
|
208
217
|
for (const entry of recents.entries(kind)) {
|
|
209
218
|
if (entry.kind === 'command') {
|
|
210
|
-
const display = commandDisplay(commands, entry.id);
|
|
219
|
+
const display = commandDisplay(commands, entry.id, entry.args);
|
|
211
220
|
if (!display) {
|
|
212
221
|
continue;
|
|
213
222
|
}
|
|
214
223
|
items.push({
|
|
215
224
|
kind: 'command',
|
|
216
|
-
key: `recent:command:${entry.id}`,
|
|
225
|
+
key: `recent:command:${entry.id}:${JSON.stringify(entry.args ?? null)}`,
|
|
217
226
|
label: display.label,
|
|
218
227
|
matchIndices: [],
|
|
219
228
|
caption: display.caption || undefined,
|
|
220
229
|
execute: () => {
|
|
221
|
-
executeCommand(commands, recents, entry.id);
|
|
230
|
+
executeCommand(commands, recents, entry.id, entry.args);
|
|
222
231
|
}
|
|
223
232
|
});
|
|
224
233
|
} else {
|
|
@@ -240,11 +249,64 @@ function buildRecentItems(
|
|
|
240
249
|
return items;
|
|
241
250
|
}
|
|
242
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Match the query against the palette's items first — their labels carry each
|
|
254
|
+
* item's args (one "Use Theme: …" per theme, one row per font-size key), which
|
|
255
|
+
* a raw registry scan can never produce — then against registry commands the
|
|
256
|
+
* palette doesn't present. Palette-covered ids are skipped in the registry
|
|
257
|
+
* pass so a command never also surfaces under its argless label.
|
|
258
|
+
*/
|
|
243
259
|
function matchCommands(options: IComputeOptions, term: string): IOmniboxItem[] {
|
|
244
|
-
const { commands, recents } = options;
|
|
260
|
+
const { commands, paletteItems, recents } = options;
|
|
245
261
|
const query = term.toLowerCase();
|
|
246
262
|
const scored: Array<{ score: number; item: IOmniboxItem }> = [];
|
|
263
|
+
const inPalette = new Set<string>();
|
|
264
|
+
const seenKeys = new Set<string>();
|
|
265
|
+
for (const item of paletteItems) {
|
|
266
|
+
inPalette.add(item.command);
|
|
267
|
+
const { command: id, args } = item;
|
|
268
|
+
let label = '';
|
|
269
|
+
let visible = true;
|
|
270
|
+
let caption = '';
|
|
271
|
+
try {
|
|
272
|
+
// An item's accessors can throw if the command assumes a context (e.g.
|
|
273
|
+
// an active notebook) the omnibox doesn't provide; skip those.
|
|
274
|
+
label = item.label;
|
|
275
|
+
visible = item.isVisible;
|
|
276
|
+
caption = item.caption;
|
|
277
|
+
} catch {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (!label || !visible) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const key = `command:${id}:${JSON.stringify(args)}`;
|
|
284
|
+
if (seenKeys.has(key)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
seenKeys.add(key);
|
|
288
|
+
const match = StringExt.matchSumOfSquares(label.toLowerCase(), query);
|
|
289
|
+
if (!match) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
scored.push({
|
|
293
|
+
score: match.score,
|
|
294
|
+
item: {
|
|
295
|
+
kind: 'command',
|
|
296
|
+
key,
|
|
297
|
+
label,
|
|
298
|
+
matchIndices: match.indices,
|
|
299
|
+
caption: caption || undefined,
|
|
300
|
+
execute: () => {
|
|
301
|
+
executeCommand(commands, recents, id, args);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
247
306
|
for (const id of commands.listCommands()) {
|
|
307
|
+
if (inPalette.has(id)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
248
310
|
let label = '';
|
|
249
311
|
let visible = true;
|
|
250
312
|
let caption = '';
|
package/src/omnibox/recents.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IStateDB } from '@jupyterlab/statedb';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Kinds of omnibox results whose uses are recorded.
|
|
@@ -15,6 +15,19 @@ interface IRecentEntry {
|
|
|
15
15
|
* The command id, or the workspace-relative file path.
|
|
16
16
|
*/
|
|
17
17
|
id: string;
|
|
18
|
+
/**
|
|
19
|
+
* Arguments the command ran with, for palette-style entries whose label
|
|
20
|
+
* and behavior depend on them (e.g. one "Use Theme: …" per theme).
|
|
21
|
+
*/
|
|
22
|
+
args?: ReadonlyPartialJSONObject;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Identity of an entry: two uses only match when kind, id and args all do,
|
|
27
|
+
* so runs of one command with different args stay distinct entries.
|
|
28
|
+
*/
|
|
29
|
+
function entryKey(entry: IRecentEntry): string {
|
|
30
|
+
return `${entry.kind}\0${entry.id}\0${JSON.stringify(entry.args ?? null)}`;
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
/**
|
|
@@ -65,13 +78,15 @@ export class OmniboxRecents {
|
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
/**
|
|
68
|
-
* Record a use of `id
|
|
81
|
+
* Record a use of `id` (with the args it ran with, if any), moving it to
|
|
82
|
+
* the front of its kind's list.
|
|
69
83
|
*/
|
|
70
|
-
touch(kind: RecentKind, id: string): void {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
);
|
|
74
|
-
|
|
84
|
+
touch(kind: RecentKind, id: string, args?: ReadonlyPartialJSONObject): void {
|
|
85
|
+
const entry: IRecentEntry =
|
|
86
|
+
args === undefined ? { kind, id } : { kind, id, args };
|
|
87
|
+
const key = entryKey(entry);
|
|
88
|
+
const rest = this._entries.filter(existing => entryKey(existing) !== key);
|
|
89
|
+
this._entries = this._trim([entry, ...rest]);
|
|
75
90
|
this._save();
|
|
76
91
|
}
|
|
77
92
|
|
|
@@ -90,23 +105,35 @@ export class OmniboxRecents {
|
|
|
90
105
|
return;
|
|
91
106
|
}
|
|
92
107
|
const persisted: IRecentEntry[] = Array.isArray(fetched)
|
|
93
|
-
? fetched.filter(
|
|
94
|
-
(entry
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
? fetched.filter((entry): entry is IRecentEntry => {
|
|
109
|
+
if (!entry || typeof entry !== 'object') {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const candidate = entry as IRecentEntry;
|
|
113
|
+
if (candidate.kind !== 'command' && candidate.kind !== 'file') {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (typeof candidate.id !== 'string') {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const args: unknown = candidate.args;
|
|
120
|
+
return (
|
|
121
|
+
args === undefined ||
|
|
122
|
+
(typeof args === 'object' && args !== null && !Array.isArray(args))
|
|
123
|
+
);
|
|
124
|
+
})
|
|
101
125
|
: [];
|
|
126
|
+
const known = new Set(this._entries.map(entryKey));
|
|
102
127
|
const merged = [...this._entries];
|
|
103
128
|
for (const entry of persisted) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
const key = entryKey(entry);
|
|
130
|
+
if (!known.has(key)) {
|
|
131
|
+
known.add(key);
|
|
132
|
+
merged.push(
|
|
133
|
+
entry.args === undefined
|
|
134
|
+
? { kind: entry.kind, id: entry.id }
|
|
135
|
+
: { kind: entry.kind, id: entry.id, args: entry.args }
|
|
136
|
+
);
|
|
110
137
|
}
|
|
111
138
|
}
|
|
112
139
|
this._entries = this._trim(merged);
|
|
@@ -128,10 +155,11 @@ export class OmniboxRecents {
|
|
|
128
155
|
if (!this._state) {
|
|
129
156
|
return;
|
|
130
157
|
}
|
|
131
|
-
const value = this._entries.map<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
158
|
+
const value = this._entries.map<ReadonlyPartialJSONObject>(entry =>
|
|
159
|
+
entry.args === undefined
|
|
160
|
+
? { kind: entry.kind, id: entry.id }
|
|
161
|
+
: { kind: entry.kind, id: entry.id, args: entry.args }
|
|
162
|
+
);
|
|
135
163
|
this._state.save(STATE_KEY, value).catch(reason => {
|
|
136
164
|
console.error('xtralab omnibox: failed to save recents', reason);
|
|
137
165
|
});
|
package/src/omnibox/widget.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import type { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
|
2
2
|
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
3
3
|
import { LabIcon, ReactWidget } from '@jupyterlab/ui-components';
|
|
4
4
|
import type { CommandRegistry } from '@lumino/commands';
|
|
5
|
+
import type { CommandPalette } from '@lumino/widgets';
|
|
5
6
|
import * as React from 'react';
|
|
6
7
|
|
|
7
8
|
import type { IAgent } from '../launcher/agents';
|
|
@@ -68,6 +69,7 @@ function renderIcon(icon?: LabIcon): React.ReactNode {
|
|
|
68
69
|
function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
|
|
69
70
|
const {
|
|
70
71
|
commands,
|
|
72
|
+
paletteItems,
|
|
71
73
|
docRegistry,
|
|
72
74
|
agents,
|
|
73
75
|
placeholder,
|
|
@@ -103,13 +105,14 @@ function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
|
|
|
103
105
|
computeSections({
|
|
104
106
|
query,
|
|
105
107
|
commands,
|
|
108
|
+
paletteItems,
|
|
106
109
|
docRegistry,
|
|
107
110
|
agents,
|
|
108
111
|
files,
|
|
109
112
|
recents,
|
|
110
113
|
trans
|
|
111
114
|
}),
|
|
112
|
-
[query, commands, docRegistry, agents, files, recents, trans]
|
|
115
|
+
[query, commands, paletteItems, docRegistry, agents, files, recents, trans]
|
|
113
116
|
);
|
|
114
117
|
const flat = React.useMemo(
|
|
115
118
|
() => [
|
|
@@ -334,6 +337,11 @@ export namespace OmniboxWidget {
|
|
|
334
337
|
*/
|
|
335
338
|
export interface IOptions {
|
|
336
339
|
commands: CommandRegistry;
|
|
340
|
+
/**
|
|
341
|
+
* Snapshot of the command palette's items, read once when the overlay
|
|
342
|
+
* opens; empty when the palette isn't available.
|
|
343
|
+
*/
|
|
344
|
+
paletteItems: ReadonlyArray<CommandPalette.IItem>;
|
|
337
345
|
docRegistry: DocumentRegistry;
|
|
338
346
|
/**
|
|
339
347
|
* Snapshot of the available agents, read once when the overlay opens.
|
package/src/svg.d.ts
ADDED
package/style/about.css
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* About dialog (xtralab:about). The header block is the dialog title, so it
|
|
3
|
+
* inherits the jp-Dialog-header type (size2/600 under chrome.css) — the
|
|
4
|
+
* wordmark scales up from there and the version line drops back to body
|
|
5
|
+
* size, weight and a muted color.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.jp-xtralab-About-header {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: 14px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.jp-xtralab-About-logo {
|
|
15
|
+
flex-shrink: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.jp-xtralab-About-header-info {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
align-items: flex-start;
|
|
22
|
+
gap: 2px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.jp-xtralab-About-wordmark {
|
|
26
|
+
font-size: var(--jp-ui-font-size3);
|
|
27
|
+
font-weight: 700;
|
|
28
|
+
letter-spacing: -0.01em;
|
|
29
|
+
line-height: 1.2;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.jp-xtralab-About-version {
|
|
33
|
+
font-size: var(--jp-ui-font-size1);
|
|
34
|
+
font-weight: 400;
|
|
35
|
+
color: var(--jp-ui-font-color2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.jp-xtralab-About-body {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
min-width: 360px;
|
|
42
|
+
font-size: var(--jp-ui-font-size1);
|
|
43
|
+
color: var(--jp-ui-font-color1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.jp-xtralab-About-basedOn {
|
|
47
|
+
color: var(--jp-ui-font-color2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.jp-xtralab-About-externalLinks {
|
|
51
|
+
padding-top: 12px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.jp-xtralab-About-externalLinks > a {
|
|
55
|
+
color: var(--jp-content-link-color);
|
|
56
|
+
text-decoration: none;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.jp-xtralab-About-externalLinks > a:hover {
|
|
60
|
+
text-decoration: underline;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.jp-xtralab-About-copyright {
|
|
64
|
+
padding-top: 12px;
|
|
65
|
+
color: var(--jp-ui-font-color3);
|
|
66
|
+
}
|
package/style/index.css
CHANGED