xtralab 0.11.0 → 0.12.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 +37 -229
- package/lib/agentSessions.d.ts +6 -2
- package/lib/askAgent/editorSelection.d.ts +53 -0
- package/lib/askAgent/editorSelection.js +95 -0
- package/lib/askAgent/icons.d.ts +7 -0
- package/lib/askAgent/icons.js +15 -0
- package/lib/askAgent/index.d.ts +32 -0
- package/lib/askAgent/index.js +741 -0
- package/lib/askAgent/popup.d.ts +83 -0
- package/lib/askAgent/popup.js +221 -0
- package/lib/askAgent/prompt.d.ts +24 -0
- package/lib/askAgent/prompt.js +116 -0
- package/lib/askAgent/queue.d.ts +93 -0
- package/lib/askAgent/queue.js +207 -0
- package/lib/askAgent/queuePanel.d.ts +76 -0
- package/lib/askAgent/queuePanel.js +188 -0
- package/lib/askAgent/targetPicker.d.ts +58 -0
- package/lib/askAgent/targetPicker.js +74 -0
- package/lib/askAgent/tokens.d.ts +124 -0
- package/lib/askAgent/tokens.js +13 -0
- package/lib/commandBar/index.d.ts +12 -8
- package/lib/commandBar/index.js +58 -41
- package/lib/editorBreadcrumbs/index.js +9 -4
- package/lib/editorBreadcrumbs/widget.d.ts +7 -7
- package/lib/editorBreadcrumbs/widget.js +6 -8
- package/lib/fileBrowser/commands.d.ts +2 -8
- package/lib/fileBrowser/commands.js +39 -30
- package/lib/fileBrowser/contents.d.ts +8 -3
- package/lib/fileBrowser/fileBrowser.d.ts +2 -1
- package/lib/fileBrowser/widget.d.ts +5 -9
- package/lib/fileBrowser/widget.js +1 -1
- package/lib/fileTypeIcons/index.js +5 -2
- package/lib/git/api.d.ts +0 -26
- package/lib/git/api.js +2 -44
- package/lib/git/askRequest.d.ts +23 -0
- package/lib/git/askRequest.js +90 -0
- package/lib/git/commands.d.ts +14 -2
- package/lib/git/commands.js +16 -13
- package/lib/git/diffModel.d.ts +1 -9
- package/lib/git/diffModel.js +1 -1
- package/lib/git/diffProvider.js +8 -3
- package/lib/git/diffSurface.d.ts +19 -2
- package/lib/git/diffSurface.js +64 -18
- package/lib/git/diffTheme.d.ts +0 -4
- package/lib/git/diffTheme.js +1 -1
- package/lib/git/diffWidget.d.ts +9 -0
- package/lib/git/diffWidget.js +38 -10
- package/lib/git/imageDiff.d.ts +17 -4
- package/lib/git/imageDiff.js +90 -30
- package/lib/git/index.js +7 -2
- package/lib/git/notebookDiff.d.ts +10 -67
- package/lib/git/notebookDiff.js +62 -69
- package/lib/git/tokens.d.ts +5 -2
- package/lib/highlight/index.js +12 -8
- package/lib/index.js +2 -0
- package/lib/launcher/agents.d.ts +11 -1
- package/lib/launcher/agents.js +26 -8
- package/lib/launcher/commands.d.ts +6 -2
- package/lib/launcher/dashboard.d.ts +8 -1
- package/lib/launcher/dashboard.js +39 -33
- package/lib/launcher/editorRegistry.js +5 -3
- package/lib/launcher/editors.d.ts +24 -7
- package/lib/launcher/editors.js +14 -5
- package/lib/launcher/icons.d.ts +0 -37
- package/lib/launcher/icons.js +11 -12
- package/lib/launcher/index.js +5 -2
- package/lib/launcher/invocation.d.ts +0 -15
- package/lib/launcher/invocation.js +1 -1
- package/lib/launcher/schemaDefaults.d.ts +3 -1
- package/lib/launcher/schemaDefaults.js +3 -1
- package/lib/mimeData.d.ts +7 -0
- package/lib/mimeData.js +16 -0
- package/lib/omnibox/index.d.ts +4 -0
- package/lib/omnibox/index.js +43 -2
- package/lib/omnibox/model.d.ts +43 -14
- package/lib/omnibox/model.js +117 -14
- package/lib/omnibox/recents.d.ts +65 -0
- package/lib/omnibox/recents.js +109 -0
- package/lib/omnibox/tokens.d.ts +6 -2
- package/lib/omnibox/widget.d.ts +23 -6
- package/lib/omnibox/widget.js +26 -7
- package/lib/showOutput/index.js +15 -18
- package/lib/terminals/agentTerminals.d.ts +63 -0
- package/lib/terminals/agentTerminals.js +157 -0
- package/lib/terminals/index.d.ts +7 -1
- package/lib/terminals/index.js +50 -30
- package/lib/terminals/model.d.ts +11 -2
- package/lib/terminals/model.js +27 -8
- package/lib/terminals/tokens.d.ts +66 -0
- package/lib/terminals/tokens.js +7 -0
- package/lib/terminals/widget.d.ts +3 -6
- package/lib/terminals/widget.js +1 -1
- package/lib/topBar/index.js +3 -3
- package/lib/walkthrough/index.js +14 -3
- package/lib/walkthrough/panel.d.ts +12 -4
- package/lib/walkthrough/panel.js +9 -3
- package/package.json +49 -51
- package/schema/command-bar.json +10 -0
- package/schema/launcher.json +4 -4
- package/schema/omnibox.json +15 -0
- package/src/agentSessions.ts +6 -2
- package/src/askAgent/editorSelection.ts +138 -0
- package/src/askAgent/icons.ts +16 -0
- package/src/askAgent/index.ts +915 -0
- package/src/askAgent/popup.tsx +407 -0
- package/src/askAgent/prompt.ts +144 -0
- package/src/askAgent/queue.ts +269 -0
- package/src/askAgent/queuePanel.tsx +418 -0
- package/src/askAgent/targetPicker.tsx +209 -0
- package/src/askAgent/tokens.ts +139 -0
- package/src/commandBar/index.ts +75 -49
- package/src/editorBreadcrumbs/index.ts +14 -4
- package/src/editorBreadcrumbs/widget.ts +11 -9
- package/src/fileBrowser/commands.ts +45 -31
- package/src/fileBrowser/contents.ts +7 -3
- package/src/fileBrowser/fileBrowser.tsx +1 -1
- package/src/fileBrowser/widget.tsx +5 -3
- package/src/fileTypeIcons/index.ts +6 -3
- package/src/git/api.ts +2 -61
- package/src/git/askRequest.ts +113 -0
- package/src/git/commands.ts +41 -14
- package/src/git/diffModel.ts +1 -1
- package/src/git/diffProvider.tsx +11 -3
- package/src/git/diffSurface.tsx +114 -22
- package/src/git/diffTheme.ts +1 -1
- package/src/git/diffWidget.tsx +54 -7
- package/src/git/imageDiff.tsx +169 -58
- package/src/git/index.ts +10 -2
- package/src/git/notebookDiff.tsx +91 -57
- package/src/git/tokens.ts +4 -2
- package/src/highlight/index.ts +12 -8
- package/src/index.ts +2 -0
- package/src/launcher/agents.ts +31 -7
- package/src/launcher/commands.ts +6 -2
- package/src/launcher/dashboard.tsx +86 -38
- package/src/launcher/editorRegistry.ts +10 -4
- package/src/launcher/editors.ts +34 -9
- package/src/launcher/icons.ts +11 -12
- package/src/launcher/index.ts +5 -2
- package/src/launcher/invocation.ts +1 -1
- package/src/launcher/schemaDefaults.ts +3 -1
- package/src/mimeData.ts +20 -0
- package/src/omnibox/index.ts +48 -1
- package/src/omnibox/model.ts +173 -33
- package/src/omnibox/recents.ts +155 -0
- package/src/omnibox/tokens.ts +6 -2
- package/src/omnibox/widget.tsx +54 -13
- package/src/showOutput/index.ts +20 -23
- package/src/terminals/agentTerminals.ts +238 -0
- package/src/terminals/index.ts +54 -30
- package/src/terminals/model.ts +28 -7
- package/src/terminals/tokens.ts +77 -0
- package/src/terminals/widget.tsx +4 -2
- package/src/topBar/index.ts +12 -6
- package/src/walkthrough/index.ts +19 -4
- package/src/walkthrough/panel.ts +15 -5
- package/style/askAgent.css +544 -0
- package/style/commandBar.css +12 -33
- package/style/git.css +72 -31
- package/style/index.css +1 -0
- package/style/index.js +2 -0
- package/style/topBar.css +4 -4
package/lib/omnibox/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ICommandPalette } from '@jupyterlab/apputils';
|
|
2
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
3
|
+
import { IStateDB } from '@jupyterlab/statedb';
|
|
2
4
|
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
3
5
|
import { Widget } from '@lumino/widgets';
|
|
4
6
|
import { IAgentRegistry } from '../launcher/tokens';
|
|
7
|
+
import { OmniboxRecents } from './recents';
|
|
5
8
|
import { IOmnibox, OMNIBOX_OPEN_COMMAND } from './tokens';
|
|
6
9
|
import { OmniboxWidget } from './widget';
|
|
7
10
|
const PLUGIN_ID = 'xtralab:omnibox';
|
|
@@ -12,6 +15,10 @@ const PLUGIN_ID = 'xtralab:omnibox';
|
|
|
12
15
|
* it in a fresh terminal through the launcher's `xtralab:start-agent:<id>`
|
|
13
16
|
* commands). It is opened by the top-bar command bar.
|
|
14
17
|
*
|
|
18
|
+
* Commands run and files opened through the overlay are remembered (persisted
|
|
19
|
+
* in the state database) and offered again at the top while the query is
|
|
20
|
+
* empty; the `maxNumberRecents` setting caps how many of each are kept.
|
|
21
|
+
*
|
|
15
22
|
* The agent rows come from the launcher's `IAgentRegistry`; when the launcher
|
|
16
23
|
* is disabled the omnibox still searches files and commands.
|
|
17
24
|
*/
|
|
@@ -20,11 +27,44 @@ const plugin = {
|
|
|
20
27
|
description: 'A launcher overlay that searches files and commands and prompts agents.',
|
|
21
28
|
autoStart: true,
|
|
22
29
|
provides: IOmnibox,
|
|
23
|
-
optional: [
|
|
24
|
-
|
|
30
|
+
optional: [
|
|
31
|
+
IAgentRegistry,
|
|
32
|
+
ICommandPalette,
|
|
33
|
+
ISettingRegistry,
|
|
34
|
+
IStateDB,
|
|
35
|
+
ITranslator
|
|
36
|
+
],
|
|
37
|
+
activate: (app, agentRegistry, palette, settingRegistry, state, translator) => {
|
|
25
38
|
const { commands, docRegistry } = app;
|
|
26
39
|
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
|
|
27
40
|
const placeholder = trans.__('Search files and commands, or ask an agent…');
|
|
41
|
+
const recents = new OmniboxRecents({ state });
|
|
42
|
+
// Restore the persisted recents only after the configured cap is known:
|
|
43
|
+
// `restore` trims to `maxItems`, so restoring at the constructor default
|
|
44
|
+
// would permanently drop entries beyond it whenever the user configured a
|
|
45
|
+
// larger cap and the state fetch won the race against the settings load.
|
|
46
|
+
if (settingRegistry) {
|
|
47
|
+
void settingRegistry
|
|
48
|
+
.load(PLUGIN_ID)
|
|
49
|
+
.then(settings => {
|
|
50
|
+
const apply = () => {
|
|
51
|
+
const value = settings.get('maxNumberRecents').composite;
|
|
52
|
+
if (typeof value === 'number') {
|
|
53
|
+
recents.maxItems = value;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
apply();
|
|
57
|
+
settings.changed.connect(apply);
|
|
58
|
+
void recents.restore();
|
|
59
|
+
})
|
|
60
|
+
.catch(reason => {
|
|
61
|
+
console.error(`xtralab omnibox: failed to load settings for ${PLUGIN_ID}`, reason);
|
|
62
|
+
void recents.restore();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
void recents.restore();
|
|
67
|
+
}
|
|
28
68
|
let current = null;
|
|
29
69
|
const close = () => {
|
|
30
70
|
const widget = current;
|
|
@@ -41,6 +81,7 @@ const plugin = {
|
|
|
41
81
|
agents: agentRegistry ? agentRegistry.agents : [],
|
|
42
82
|
placeholder,
|
|
43
83
|
initialQuery: query !== null && query !== void 0 ? query : '',
|
|
84
|
+
recents,
|
|
44
85
|
trans,
|
|
45
86
|
onClose: close
|
|
46
87
|
});
|
package/lib/omnibox/model.d.ts
CHANGED
|
@@ -3,43 +3,72 @@ import type { TranslationBundle } from '@jupyterlab/translation';
|
|
|
3
3
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
4
4
|
import type { CommandRegistry } from '@lumino/commands';
|
|
5
5
|
import type { IAgent } from '../launcher/agents';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import type { OmniboxRecents } from './recents';
|
|
7
|
+
/**
|
|
8
|
+
* The source a result row came from, used to group rows under a header.
|
|
9
|
+
*/
|
|
10
|
+
type OmniboxItemKind = 'command' | 'file' | 'agent';
|
|
11
|
+
/**
|
|
12
|
+
* A single result row, with the action it runs when chosen.
|
|
13
|
+
*/
|
|
9
14
|
export interface IOmniboxItem {
|
|
10
15
|
kind: OmniboxItemKind;
|
|
11
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Stable React key.
|
|
18
|
+
*/
|
|
12
19
|
key: string;
|
|
13
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Primary text shown on the row.
|
|
22
|
+
*/
|
|
14
23
|
label: string;
|
|
15
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Indices of matched characters in `label`, for highlighting. May be empty.
|
|
26
|
+
*/
|
|
16
27
|
matchIndices: readonly number[];
|
|
17
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Optional secondary text shown trailing the label.
|
|
30
|
+
*/
|
|
18
31
|
caption?: string;
|
|
19
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Optional leading icon.
|
|
34
|
+
*/
|
|
20
35
|
icon?: LabIcon;
|
|
21
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Run the row's action.
|
|
38
|
+
*/
|
|
22
39
|
execute: () => void;
|
|
23
40
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Results grouped by source; each group is already capped and sorted.
|
|
43
|
+
*/
|
|
44
|
+
interface IOmniboxSections {
|
|
45
|
+
/**
|
|
46
|
+
* Recently used commands and files, shown while the term is empty.
|
|
47
|
+
*/
|
|
48
|
+
recent: IOmniboxItem[];
|
|
26
49
|
commands: IOmniboxItem[];
|
|
27
50
|
files: IOmniboxItem[];
|
|
28
51
|
agents: IOmniboxItem[];
|
|
29
52
|
}
|
|
30
|
-
|
|
53
|
+
interface IComputeOptions {
|
|
31
54
|
query: string;
|
|
32
55
|
commands: CommandRegistry;
|
|
33
56
|
docRegistry: DocumentRegistry;
|
|
34
57
|
agents: IAgent[];
|
|
35
58
|
files: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Recently-used tracker; `null` disables the recent rows and recording.
|
|
61
|
+
*/
|
|
62
|
+
recents: OmniboxRecents | null;
|
|
36
63
|
trans: TranslationBundle;
|
|
37
64
|
}
|
|
38
65
|
/**
|
|
39
66
|
* Build the grouped result set for a query. A leading `>` searches only
|
|
40
67
|
* commands and a leading `/` only files; otherwise commands and files are
|
|
41
68
|
* fuzzy-matched and every prompt-capable agent is offered an "Ask" row
|
|
42
|
-
* carrying the query as its prompt. An empty term yields
|
|
43
|
-
* shows a hint
|
|
69
|
+
* carrying the query as its prompt. An empty term yields the recently used
|
|
70
|
+
* rows for the active mode instead (and the widget shows a hint when there
|
|
71
|
+
* are none).
|
|
44
72
|
*/
|
|
45
73
|
export declare function computeSections(options: IComputeOptions): IOmniboxSections;
|
|
74
|
+
export {};
|
package/lib/omnibox/model.js
CHANGED
|
@@ -14,11 +14,12 @@ const FILE_PREFIX = '/';
|
|
|
14
14
|
* Build the grouped result set for a query. A leading `>` searches only
|
|
15
15
|
* commands and a leading `/` only files; otherwise commands and files are
|
|
16
16
|
* fuzzy-matched and every prompt-capable agent is offered an "Ask" row
|
|
17
|
-
* carrying the query as its prompt. An empty term yields
|
|
18
|
-
* shows a hint
|
|
17
|
+
* carrying the query as its prompt. An empty term yields the recently used
|
|
18
|
+
* rows for the active mode instead (and the widget shows a hint when there
|
|
19
|
+
* are none).
|
|
19
20
|
*/
|
|
20
21
|
export function computeSections(options) {
|
|
21
|
-
const { query, commands,
|
|
22
|
+
const { query, commands, agents, trans } = options;
|
|
22
23
|
const trimmed = query.trim();
|
|
23
24
|
let mode = 'all';
|
|
24
25
|
let term = trimmed;
|
|
@@ -31,16 +32,121 @@ export function computeSections(options) {
|
|
|
31
32
|
term = trimmed.slice(FILE_PREFIX.length).trim();
|
|
32
33
|
}
|
|
33
34
|
if (!term) {
|
|
34
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
recent: buildRecentItems(options, mode),
|
|
37
|
+
commands: [],
|
|
38
|
+
files: [],
|
|
39
|
+
agents: []
|
|
40
|
+
};
|
|
35
41
|
}
|
|
36
42
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
recent: [],
|
|
44
|
+
commands: mode === 'files' ? [] : matchCommands(options, term),
|
|
45
|
+
files: mode === 'commands' ? [] : matchFiles(options, term),
|
|
39
46
|
// Agents only in the unprefixed view; the prompt is the full typed query.
|
|
40
47
|
agents: mode === 'all' ? buildAgentItems(commands, agents, term, trans) : []
|
|
41
48
|
};
|
|
42
49
|
}
|
|
43
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Run a command and record the use on success, so failed commands never enter
|
|
52
|
+
* the recents list.
|
|
53
|
+
*/
|
|
54
|
+
function executeCommand(commands, recents, id) {
|
|
55
|
+
void commands
|
|
56
|
+
.execute(id)
|
|
57
|
+
.then(() => {
|
|
58
|
+
recents === null || recents === void 0 ? void 0 : recents.touch('command', id);
|
|
59
|
+
})
|
|
60
|
+
.catch(reason => {
|
|
61
|
+
console.error(`xtralab omnibox: command "${id}" failed`, reason);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Open a file and record the use on success.
|
|
66
|
+
*/
|
|
67
|
+
function openFile(commands, recents, path) {
|
|
68
|
+
void commands
|
|
69
|
+
.execute('docmanager:open', { path })
|
|
70
|
+
.then(() => {
|
|
71
|
+
recents === null || recents === void 0 ? void 0 : recents.touch('file', path);
|
|
72
|
+
})
|
|
73
|
+
.catch(reason => {
|
|
74
|
+
console.error(`xtralab omnibox: failed to open "${path}"`, reason);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read a command's label, visibility and caption, or `null` when the command
|
|
79
|
+
* is missing, hidden, label-less, or its accessors throw (a command's
|
|
80
|
+
* accessors can assume a context — e.g. an active notebook — the omnibox
|
|
81
|
+
* doesn't provide).
|
|
82
|
+
*/
|
|
83
|
+
function commandDisplay(commands, id) {
|
|
84
|
+
if (!commands.hasCommand(id)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const label = commands.label(id);
|
|
89
|
+
if (!label || !commands.isVisible(id)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return { label, caption: commands.caption(id) };
|
|
93
|
+
}
|
|
94
|
+
catch (_a) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The recently used rows for the empty term: both kinds interleaved by
|
|
100
|
+
* recency in the unprefixed view, or only the mode's kind under a bare `>` or
|
|
101
|
+
* `/`. Commands that no longer resolve are dropped, as are files missing from
|
|
102
|
+
* the loaded workspace listing (while the listing is still loading — or
|
|
103
|
+
* unavailable — file entries are shown as recorded).
|
|
104
|
+
*/
|
|
105
|
+
function buildRecentItems(options, mode) {
|
|
106
|
+
const { commands, docRegistry, files, recents } = options;
|
|
107
|
+
if (!recents) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const kind = mode === 'commands' ? 'command' : mode === 'files' ? 'file' : undefined;
|
|
111
|
+
const knownFiles = files.length > 0 ? new Set(files) : null;
|
|
112
|
+
const items = [];
|
|
113
|
+
for (const entry of recents.entries(kind)) {
|
|
114
|
+
if (entry.kind === 'command') {
|
|
115
|
+
const display = commandDisplay(commands, entry.id);
|
|
116
|
+
if (!display) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
items.push({
|
|
120
|
+
kind: 'command',
|
|
121
|
+
key: `recent:command:${entry.id}`,
|
|
122
|
+
label: display.label,
|
|
123
|
+
matchIndices: [],
|
|
124
|
+
caption: display.caption || undefined,
|
|
125
|
+
execute: () => {
|
|
126
|
+
executeCommand(commands, recents, entry.id);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
if (knownFiles && !knownFiles.has(entry.id)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
items.push({
|
|
135
|
+
kind: 'file',
|
|
136
|
+
key: `recent:file:${entry.id}`,
|
|
137
|
+
label: entry.id,
|
|
138
|
+
matchIndices: [],
|
|
139
|
+
icon: fileIconForPath(docRegistry, entry.id),
|
|
140
|
+
execute: () => {
|
|
141
|
+
openFile(commands, recents, entry.id);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return items;
|
|
147
|
+
}
|
|
148
|
+
function matchCommands(options, term) {
|
|
149
|
+
const { commands, recents } = options;
|
|
44
150
|
const query = term.toLowerCase();
|
|
45
151
|
const scored = [];
|
|
46
152
|
for (const id of commands.listCommands()) {
|
|
@@ -73,9 +179,7 @@ function matchCommands(commands, term) {
|
|
|
73
179
|
matchIndices: match.indices,
|
|
74
180
|
caption: caption || undefined,
|
|
75
181
|
execute: () => {
|
|
76
|
-
|
|
77
|
-
console.error(`xtralab omnibox: command "${id}" failed`, reason);
|
|
78
|
-
});
|
|
182
|
+
executeCommand(commands, recents, id);
|
|
79
183
|
}
|
|
80
184
|
}
|
|
81
185
|
});
|
|
@@ -83,7 +187,8 @@ function matchCommands(commands, term) {
|
|
|
83
187
|
scored.sort((a, b) => a.score - b.score);
|
|
84
188
|
return scored.slice(0, COMMAND_LIMIT).map(entry => entry.item);
|
|
85
189
|
}
|
|
86
|
-
function matchFiles(
|
|
190
|
+
function matchFiles(options, term) {
|
|
191
|
+
const { commands, docRegistry, files, recents } = options;
|
|
87
192
|
const query = term.toLowerCase();
|
|
88
193
|
const scored = [];
|
|
89
194
|
for (const path of files) {
|
|
@@ -100,9 +205,7 @@ function matchFiles(commands, docRegistry, files, term) {
|
|
|
100
205
|
matchIndices: indices,
|
|
101
206
|
icon: fileIconForPath(docRegistry, path),
|
|
102
207
|
execute: () => {
|
|
103
|
-
|
|
104
|
-
console.error(`xtralab omnibox: failed to open "${path}"`, reason);
|
|
105
|
-
});
|
|
208
|
+
openFile(commands, recents, path);
|
|
106
209
|
}
|
|
107
210
|
}));
|
|
108
211
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { IStateDB } from '@jupyterlab/statedb';
|
|
2
|
+
/**
|
|
3
|
+
* Kinds of omnibox results whose uses are recorded.
|
|
4
|
+
*/
|
|
5
|
+
export type RecentKind = 'command' | 'file';
|
|
6
|
+
/**
|
|
7
|
+
* A single recorded use: a command that was run or a file that was opened.
|
|
8
|
+
*/
|
|
9
|
+
interface IRecentEntry {
|
|
10
|
+
kind: RecentKind;
|
|
11
|
+
/**
|
|
12
|
+
* The command id, or the workspace-relative file path.
|
|
13
|
+
*/
|
|
14
|
+
id: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A most-recently-used list of omnibox results, generic over the result kind
|
|
18
|
+
* so commands and files (and future kinds) share one store. Entries are kept
|
|
19
|
+
* most-recent-first, capped at `maxItems` per kind, and persisted through the
|
|
20
|
+
* state database so they survive reloads. Without a state database the list
|
|
21
|
+
* still works for the lifetime of the page.
|
|
22
|
+
*/
|
|
23
|
+
export declare class OmniboxRecents {
|
|
24
|
+
constructor(options?: OmniboxRecents.IOptions);
|
|
25
|
+
/**
|
|
26
|
+
* Maximum number of entries remembered per kind. Lowering it trims the list
|
|
27
|
+
* immediately; `0` disables recents entirely.
|
|
28
|
+
*/
|
|
29
|
+
get maxItems(): number;
|
|
30
|
+
set maxItems(value: number);
|
|
31
|
+
/**
|
|
32
|
+
* The recorded entries, most recent first — all of them, or only those of
|
|
33
|
+
* one kind.
|
|
34
|
+
*/
|
|
35
|
+
entries(kind?: RecentKind): IRecentEntry[];
|
|
36
|
+
/**
|
|
37
|
+
* Record a use of `id`, moving it to the front of its kind's list.
|
|
38
|
+
*/
|
|
39
|
+
touch(kind: RecentKind, id: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* Load the persisted list. Entries recorded before the load completes stay
|
|
42
|
+
* at the front, ahead of the restored history.
|
|
43
|
+
*/
|
|
44
|
+
restore(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Keep at most `maxItems` entries of each kind, preserving order.
|
|
47
|
+
*/
|
|
48
|
+
private _trim;
|
|
49
|
+
private _save;
|
|
50
|
+
private _state;
|
|
51
|
+
private _entries;
|
|
52
|
+
private _maxItems;
|
|
53
|
+
}
|
|
54
|
+
export declare namespace OmniboxRecents {
|
|
55
|
+
/**
|
|
56
|
+
* Construction options for {@link OmniboxRecents}.
|
|
57
|
+
*/
|
|
58
|
+
interface IOptions {
|
|
59
|
+
/**
|
|
60
|
+
* The state database to persist through, if available.
|
|
61
|
+
*/
|
|
62
|
+
state?: IStateDB | null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State database key the recents list is persisted under.
|
|
3
|
+
*/
|
|
4
|
+
const STATE_KEY = 'xtralab:omnibox:recents';
|
|
5
|
+
/**
|
|
6
|
+
* A most-recently-used list of omnibox results, generic over the result kind
|
|
7
|
+
* so commands and files (and future kinds) share one store. Entries are kept
|
|
8
|
+
* most-recent-first, capped at `maxItems` per kind, and persisted through the
|
|
9
|
+
* state database so they survive reloads. Without a state database the list
|
|
10
|
+
* still works for the lifetime of the page.
|
|
11
|
+
*/
|
|
12
|
+
export class OmniboxRecents {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
var _a;
|
|
15
|
+
this._entries = [];
|
|
16
|
+
this._maxItems = 5;
|
|
17
|
+
this._state = (_a = options.state) !== null && _a !== void 0 ? _a : null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Maximum number of entries remembered per kind. Lowering it trims the list
|
|
21
|
+
* immediately; `0` disables recents entirely.
|
|
22
|
+
*/
|
|
23
|
+
get maxItems() {
|
|
24
|
+
return this._maxItems;
|
|
25
|
+
}
|
|
26
|
+
set maxItems(value) {
|
|
27
|
+
const normalized = Math.max(0, Math.floor(value));
|
|
28
|
+
if (normalized === this._maxItems) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this._maxItems = normalized;
|
|
32
|
+
const trimmed = this._trim(this._entries);
|
|
33
|
+
if (trimmed.length !== this._entries.length) {
|
|
34
|
+
this._entries = trimmed;
|
|
35
|
+
this._save();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The recorded entries, most recent first — all of them, or only those of
|
|
40
|
+
* one kind.
|
|
41
|
+
*/
|
|
42
|
+
entries(kind) {
|
|
43
|
+
return kind
|
|
44
|
+
? this._entries.filter(entry => entry.kind === kind)
|
|
45
|
+
: [...this._entries];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Record a use of `id`, moving it to the front of its kind's list.
|
|
49
|
+
*/
|
|
50
|
+
touch(kind, id) {
|
|
51
|
+
const rest = this._entries.filter(entry => !(entry.kind === kind && entry.id === id));
|
|
52
|
+
this._entries = this._trim([{ kind, id }, ...rest]);
|
|
53
|
+
this._save();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Load the persisted list. Entries recorded before the load completes stay
|
|
57
|
+
* at the front, ahead of the restored history.
|
|
58
|
+
*/
|
|
59
|
+
async restore() {
|
|
60
|
+
if (!this._state) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let fetched;
|
|
64
|
+
try {
|
|
65
|
+
fetched = await this._state.fetch(STATE_KEY);
|
|
66
|
+
}
|
|
67
|
+
catch (_a) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const persisted = Array.isArray(fetched)
|
|
71
|
+
? fetched.filter((entry) => !!entry &&
|
|
72
|
+
typeof entry === 'object' &&
|
|
73
|
+
(entry.kind === 'command' ||
|
|
74
|
+
entry.kind === 'file') &&
|
|
75
|
+
typeof entry.id === 'string')
|
|
76
|
+
: [];
|
|
77
|
+
const merged = [...this._entries];
|
|
78
|
+
for (const entry of persisted) {
|
|
79
|
+
if (!merged.some(existing => existing.kind === entry.kind && existing.id === entry.id)) {
|
|
80
|
+
merged.push({ kind: entry.kind, id: entry.id });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this._entries = this._trim(merged);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Keep at most `maxItems` entries of each kind, preserving order.
|
|
87
|
+
*/
|
|
88
|
+
_trim(entries) {
|
|
89
|
+
const counts = new Map();
|
|
90
|
+
return entries.filter(entry => {
|
|
91
|
+
var _a;
|
|
92
|
+
const count = (_a = counts.get(entry.kind)) !== null && _a !== void 0 ? _a : 0;
|
|
93
|
+
counts.set(entry.kind, count + 1);
|
|
94
|
+
return count < this._maxItems;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
_save() {
|
|
98
|
+
if (!this._state) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const value = this._entries.map(entry => ({
|
|
102
|
+
kind: entry.kind,
|
|
103
|
+
id: entry.id
|
|
104
|
+
}));
|
|
105
|
+
this._state.save(STATE_KEY, value).catch(reason => {
|
|
106
|
+
console.error('xtralab omnibox: failed to save recents', reason);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
package/lib/omnibox/tokens.d.ts
CHANGED
|
@@ -6,9 +6,13 @@ import { Token } from '@lumino/coreutils';
|
|
|
6
6
|
* by the top-bar command bar, which opens it on click.
|
|
7
7
|
*/
|
|
8
8
|
export interface IOmnibox {
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Open the overlay, optionally seeding the input with `query`.
|
|
11
|
+
*/
|
|
10
12
|
open(query?: string): void;
|
|
11
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Close the overlay if it is open.
|
|
15
|
+
*/
|
|
12
16
|
close(): void;
|
|
13
17
|
}
|
|
14
18
|
/**
|
package/lib/omnibox/widget.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { TranslationBundle } from '@jupyterlab/translation';
|
|
|
3
3
|
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
4
4
|
import type { CommandRegistry } from '@lumino/commands';
|
|
5
5
|
import type { IAgent } from '../launcher/agents';
|
|
6
|
+
import type { OmniboxRecents } from './recents';
|
|
6
7
|
/**
|
|
7
8
|
* The omnibox overlay widget. Hosts {@link OmniboxComponent} in a React root
|
|
8
9
|
* attached to `document.body`. It carries `jp-ThemedContainer` so JupyterLab's
|
|
@@ -18,19 +19,35 @@ export declare class OmniboxWidget extends ReactWidget {
|
|
|
18
19
|
* A namespace for `OmniboxWidget` statics.
|
|
19
20
|
*/
|
|
20
21
|
export declare namespace OmniboxWidget {
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Construction options for {@link OmniboxWidget}.
|
|
24
|
+
*/
|
|
22
25
|
interface IOptions {
|
|
23
26
|
commands: CommandRegistry;
|
|
24
27
|
docRegistry: DocumentRegistry;
|
|
25
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Snapshot of the available agents, read once when the overlay opens.
|
|
30
|
+
*/
|
|
26
31
|
agents: IAgent[];
|
|
27
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Placeholder text for the input.
|
|
34
|
+
*/
|
|
28
35
|
placeholder: string;
|
|
29
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Seed text for the input.
|
|
38
|
+
*/
|
|
30
39
|
initialQuery: string;
|
|
31
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Recently-used tracker; `null` disables the recent rows and recording.
|
|
42
|
+
*/
|
|
43
|
+
recents: OmniboxRecents | null;
|
|
44
|
+
/**
|
|
45
|
+
* Translation bundle for the overlay's own labels.
|
|
46
|
+
*/
|
|
32
47
|
trans: TranslationBundle;
|
|
33
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Dismiss the overlay (the plugin disposes the widget).
|
|
50
|
+
*/
|
|
34
51
|
onClose: () => void;
|
|
35
52
|
}
|
|
36
53
|
}
|
package/lib/omnibox/widget.js
CHANGED
|
@@ -2,9 +2,13 @@ import { ReactWidget } from '@jupyterlab/ui-components';
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { computeSections } from './model';
|
|
4
4
|
import { loadWorkspaceFiles } from './files';
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* DOM id of the results listbox, referenced by the input's `aria-controls`.
|
|
7
|
+
*/
|
|
6
8
|
const LIST_ID = 'jp-xtralab-Omnibox-list';
|
|
7
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Stable DOM id for the result row at `index`, for `aria-activedescendant`.
|
|
11
|
+
*/
|
|
8
12
|
function optionId(index) {
|
|
9
13
|
return `jp-xtralab-Omnibox-option-${index}`;
|
|
10
14
|
}
|
|
@@ -35,7 +39,7 @@ function renderIcon(icon) {
|
|
|
35
39
|
return icon ? (React.createElement(icon.react, { tag: "span", className: "jp-xtralab-Omnibox-itemIcon" })) : (React.createElement("span", { className: "jp-xtralab-Omnibox-itemIcon" }));
|
|
36
40
|
}
|
|
37
41
|
function OmniboxComponent(props) {
|
|
38
|
-
const { commands, docRegistry, agents, placeholder, initialQuery, trans, onClose } = props;
|
|
42
|
+
const { commands, docRegistry, agents, placeholder, initialQuery, recents, trans, onClose } = props;
|
|
39
43
|
const [query, setQuery] = React.useState(initialQuery);
|
|
40
44
|
const [files, setFiles] = React.useState([]);
|
|
41
45
|
const [active, setActive] = React.useState(0);
|
|
@@ -56,8 +60,21 @@ function OmniboxComponent(props) {
|
|
|
56
60
|
cancelled = true;
|
|
57
61
|
};
|
|
58
62
|
}, []);
|
|
59
|
-
const sections = React.useMemo(() => computeSections({
|
|
60
|
-
|
|
63
|
+
const sections = React.useMemo(() => computeSections({
|
|
64
|
+
query,
|
|
65
|
+
commands,
|
|
66
|
+
docRegistry,
|
|
67
|
+
agents,
|
|
68
|
+
files,
|
|
69
|
+
recents,
|
|
70
|
+
trans
|
|
71
|
+
}), [query, commands, docRegistry, agents, files, recents, trans]);
|
|
72
|
+
const flat = React.useMemo(() => [
|
|
73
|
+
...sections.recent,
|
|
74
|
+
...sections.commands,
|
|
75
|
+
...sections.files,
|
|
76
|
+
...sections.agents
|
|
77
|
+
], [sections]);
|
|
61
78
|
const hasResults = flat.length > 0;
|
|
62
79
|
// Reset the highlighted row whenever the result set changes, and keep the
|
|
63
80
|
// index in range when results shrink.
|
|
@@ -125,7 +142,8 @@ function OmniboxComponent(props) {
|
|
|
125
142
|
item.caption ? (React.createElement("span", { className: "jp-xtralab-Omnibox-itemCaption" }, item.caption)) : null));
|
|
126
143
|
})));
|
|
127
144
|
};
|
|
128
|
-
const
|
|
145
|
+
const commandsStart = sections.recent.length;
|
|
146
|
+
const filesStart = commandsStart + sections.commands.length;
|
|
129
147
|
const agentsStart = filesStart + sections.files.length;
|
|
130
148
|
return (React.createElement("div", { className: "jp-xtralab-Omnibox-overlay", onMouseDown: event => {
|
|
131
149
|
if (event.target === event.currentTarget) {
|
|
@@ -135,7 +153,8 @@ function OmniboxComponent(props) {
|
|
|
135
153
|
React.createElement("div", { className: "jp-xtralab-Omnibox-panel", role: "dialog", "aria-modal": "true", "aria-label": placeholder, onMouseDown: event => event.stopPropagation() },
|
|
136
154
|
React.createElement("input", { ref: inputRef, className: "jp-xtralab-Omnibox-input", type: "text", spellCheck: false, placeholder: placeholder, "aria-label": placeholder, role: "combobox", "aria-autocomplete": "list", "aria-expanded": hasResults, "aria-controls": LIST_ID, "aria-activedescendant": hasResults ? optionId(active) : undefined, value: query, onChange: event => setQuery(event.target.value), onKeyDown: onKeyDown }),
|
|
137
155
|
React.createElement("div", { className: "jp-xtralab-Omnibox-results", id: LIST_ID, role: hasResults ? 'listbox' : undefined, ref: listRef }, hasResults ? (React.createElement(React.Fragment, null,
|
|
138
|
-
renderSection(trans.__('
|
|
156
|
+
renderSection(trans.__('Recently used'), sections.recent, 0),
|
|
157
|
+
renderSection(trans.__('Commands'), sections.commands, commandsStart),
|
|
139
158
|
renderSection(trans.__('Files'), sections.files, filesStart),
|
|
140
159
|
renderSection(trans.__('Ask an agent'), sections.agents, agentsStart))) : (React.createElement("div", { className: "jp-xtralab-Omnibox-empty" }, query.trim()
|
|
141
160
|
? trans.__('No matches.')
|