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/src/omnibox/index.ts
CHANGED
|
@@ -3,11 +3,14 @@ import {
|
|
|
3
3
|
JupyterFrontEndPlugin
|
|
4
4
|
} from '@jupyterlab/application';
|
|
5
5
|
import { ICommandPalette } from '@jupyterlab/apputils';
|
|
6
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
7
|
+
import { IStateDB } from '@jupyterlab/statedb';
|
|
6
8
|
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
7
9
|
import { Widget } from '@lumino/widgets';
|
|
8
10
|
|
|
9
11
|
import { IAgentRegistry } from '../launcher/tokens';
|
|
10
12
|
|
|
13
|
+
import { OmniboxRecents } from './recents';
|
|
11
14
|
import { IOmnibox, OMNIBOX_OPEN_COMMAND } from './tokens';
|
|
12
15
|
import { OmniboxWidget } from './widget';
|
|
13
16
|
|
|
@@ -20,6 +23,10 @@ const PLUGIN_ID = 'xtralab:omnibox';
|
|
|
20
23
|
* it in a fresh terminal through the launcher's `xtralab:start-agent:<id>`
|
|
21
24
|
* commands). It is opened by the top-bar command bar.
|
|
22
25
|
*
|
|
26
|
+
* Commands run and files opened through the overlay are remembered (persisted
|
|
27
|
+
* in the state database) and offered again at the top while the query is
|
|
28
|
+
* empty; the `maxNumberRecents` setting caps how many of each are kept.
|
|
29
|
+
*
|
|
23
30
|
* The agent rows come from the launcher's `IAgentRegistry`; when the launcher
|
|
24
31
|
* is disabled the omnibox still searches files and commands.
|
|
25
32
|
*/
|
|
@@ -29,17 +36,56 @@ const plugin: JupyterFrontEndPlugin<IOmnibox> = {
|
|
|
29
36
|
'A launcher overlay that searches files and commands and prompts agents.',
|
|
30
37
|
autoStart: true,
|
|
31
38
|
provides: IOmnibox,
|
|
32
|
-
optional: [
|
|
39
|
+
optional: [
|
|
40
|
+
IAgentRegistry,
|
|
41
|
+
ICommandPalette,
|
|
42
|
+
ISettingRegistry,
|
|
43
|
+
IStateDB,
|
|
44
|
+
ITranslator
|
|
45
|
+
],
|
|
33
46
|
activate: (
|
|
34
47
|
app: JupyterFrontEnd,
|
|
35
48
|
agentRegistry: IAgentRegistry | null,
|
|
36
49
|
palette: ICommandPalette | null,
|
|
50
|
+
settingRegistry: ISettingRegistry | null,
|
|
51
|
+
state: IStateDB | null,
|
|
37
52
|
translator: ITranslator | null
|
|
38
53
|
): IOmnibox => {
|
|
39
54
|
const { commands, docRegistry } = app;
|
|
40
55
|
const trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
41
56
|
const placeholder = trans.__('Search files and commands, or ask an agent…');
|
|
42
57
|
|
|
58
|
+
const recents = new OmniboxRecents({ state });
|
|
59
|
+
|
|
60
|
+
// Restore the persisted recents only after the configured cap is known:
|
|
61
|
+
// `restore` trims to `maxItems`, so restoring at the constructor default
|
|
62
|
+
// would permanently drop entries beyond it whenever the user configured a
|
|
63
|
+
// larger cap and the state fetch won the race against the settings load.
|
|
64
|
+
if (settingRegistry) {
|
|
65
|
+
void settingRegistry
|
|
66
|
+
.load(PLUGIN_ID)
|
|
67
|
+
.then(settings => {
|
|
68
|
+
const apply = (): void => {
|
|
69
|
+
const value = settings.get('maxNumberRecents').composite;
|
|
70
|
+
if (typeof value === 'number') {
|
|
71
|
+
recents.maxItems = value;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
apply();
|
|
75
|
+
settings.changed.connect(apply);
|
|
76
|
+
void recents.restore();
|
|
77
|
+
})
|
|
78
|
+
.catch(reason => {
|
|
79
|
+
console.error(
|
|
80
|
+
`xtralab omnibox: failed to load settings for ${PLUGIN_ID}`,
|
|
81
|
+
reason
|
|
82
|
+
);
|
|
83
|
+
void recents.restore();
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
void recents.restore();
|
|
87
|
+
}
|
|
88
|
+
|
|
43
89
|
let current: OmniboxWidget | null = null;
|
|
44
90
|
|
|
45
91
|
const close = (): void => {
|
|
@@ -58,6 +104,7 @@ const plugin: JupyterFrontEndPlugin<IOmnibox> = {
|
|
|
58
104
|
agents: agentRegistry ? agentRegistry.agents : [],
|
|
59
105
|
placeholder,
|
|
60
106
|
initialQuery: query ?? '',
|
|
107
|
+
recents,
|
|
61
108
|
trans,
|
|
62
109
|
onClose: close
|
|
63
110
|
});
|
package/src/omnibox/model.ts
CHANGED
|
@@ -7,39 +7,67 @@ import type { CommandRegistry } from '@lumino/commands';
|
|
|
7
7
|
import type { IAgent } from '../launcher/agents';
|
|
8
8
|
import { agentCommandId } from '../launcher/tokens';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
export type OmniboxItemKind = 'command' | 'file' | 'agent';
|
|
10
|
+
import type { OmniboxRecents, RecentKind } from './recents';
|
|
12
11
|
|
|
13
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The source a result row came from, used to group rows under a header.
|
|
14
|
+
*/
|
|
15
|
+
type OmniboxItemKind = 'command' | 'file' | 'agent';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A single result row, with the action it runs when chosen.
|
|
19
|
+
*/
|
|
14
20
|
export interface IOmniboxItem {
|
|
15
21
|
kind: OmniboxItemKind;
|
|
16
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Stable React key.
|
|
24
|
+
*/
|
|
17
25
|
key: string;
|
|
18
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Primary text shown on the row.
|
|
28
|
+
*/
|
|
19
29
|
label: string;
|
|
20
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Indices of matched characters in `label`, for highlighting. May be empty.
|
|
32
|
+
*/
|
|
21
33
|
matchIndices: readonly number[];
|
|
22
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Optional secondary text shown trailing the label.
|
|
36
|
+
*/
|
|
23
37
|
caption?: string;
|
|
24
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Optional leading icon.
|
|
40
|
+
*/
|
|
25
41
|
icon?: LabIcon;
|
|
26
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Run the row's action.
|
|
44
|
+
*/
|
|
27
45
|
execute: () => void;
|
|
28
46
|
}
|
|
29
47
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Results grouped by source; each group is already capped and sorted.
|
|
50
|
+
*/
|
|
51
|
+
interface IOmniboxSections {
|
|
52
|
+
/**
|
|
53
|
+
* Recently used commands and files, shown while the term is empty.
|
|
54
|
+
*/
|
|
55
|
+
recent: IOmniboxItem[];
|
|
32
56
|
commands: IOmniboxItem[];
|
|
33
57
|
files: IOmniboxItem[];
|
|
34
58
|
agents: IOmniboxItem[];
|
|
35
59
|
}
|
|
36
60
|
|
|
37
|
-
|
|
61
|
+
interface IComputeOptions {
|
|
38
62
|
query: string;
|
|
39
63
|
commands: CommandRegistry;
|
|
40
64
|
docRegistry: DocumentRegistry;
|
|
41
65
|
agents: IAgent[];
|
|
42
66
|
files: string[];
|
|
67
|
+
/**
|
|
68
|
+
* Recently-used tracker; `null` disables the recent rows and recording.
|
|
69
|
+
*/
|
|
70
|
+
recents: OmniboxRecents | null;
|
|
43
71
|
trans: TranslationBundle;
|
|
44
72
|
}
|
|
45
73
|
|
|
@@ -60,11 +88,12 @@ type Mode = 'all' | 'commands' | 'files';
|
|
|
60
88
|
* Build the grouped result set for a query. A leading `>` searches only
|
|
61
89
|
* commands and a leading `/` only files; otherwise commands and files are
|
|
62
90
|
* fuzzy-matched and every prompt-capable agent is offered an "Ask" row
|
|
63
|
-
* carrying the query as its prompt. An empty term yields
|
|
64
|
-
* shows a hint
|
|
91
|
+
* carrying the query as its prompt. An empty term yields the recently used
|
|
92
|
+
* rows for the active mode instead (and the widget shows a hint when there
|
|
93
|
+
* are none).
|
|
65
94
|
*/
|
|
66
95
|
export function computeSections(options: IComputeOptions): IOmniboxSections {
|
|
67
|
-
const { query, commands,
|
|
96
|
+
const { query, commands, agents, trans } = options;
|
|
68
97
|
const trimmed = query.trim();
|
|
69
98
|
|
|
70
99
|
let mode: Mode = 'all';
|
|
@@ -78,22 +107,141 @@ export function computeSections(options: IComputeOptions): IOmniboxSections {
|
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
if (!term) {
|
|
81
|
-
return {
|
|
110
|
+
return {
|
|
111
|
+
recent: buildRecentItems(options, mode),
|
|
112
|
+
commands: [],
|
|
113
|
+
files: [],
|
|
114
|
+
agents: []
|
|
115
|
+
};
|
|
82
116
|
}
|
|
83
117
|
|
|
84
118
|
return {
|
|
85
|
-
|
|
86
|
-
files:
|
|
87
|
-
|
|
119
|
+
recent: [],
|
|
120
|
+
commands: mode === 'files' ? [] : matchCommands(options, term),
|
|
121
|
+
files: mode === 'commands' ? [] : matchFiles(options, term),
|
|
88
122
|
// Agents only in the unprefixed view; the prompt is the full typed query.
|
|
89
123
|
agents: mode === 'all' ? buildAgentItems(commands, agents, term, trans) : []
|
|
90
124
|
};
|
|
91
125
|
}
|
|
92
126
|
|
|
93
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Run a command and record the use on success, so failed commands never enter
|
|
129
|
+
* the recents list.
|
|
130
|
+
*/
|
|
131
|
+
function executeCommand(
|
|
94
132
|
commands: CommandRegistry,
|
|
95
|
-
|
|
133
|
+
recents: OmniboxRecents | null,
|
|
134
|
+
id: string
|
|
135
|
+
): void {
|
|
136
|
+
void commands
|
|
137
|
+
.execute(id)
|
|
138
|
+
.then(() => {
|
|
139
|
+
recents?.touch('command', id);
|
|
140
|
+
})
|
|
141
|
+
.catch(reason => {
|
|
142
|
+
console.error(`xtralab omnibox: command "${id}" failed`, reason);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Open a file and record the use on success.
|
|
148
|
+
*/
|
|
149
|
+
function openFile(
|
|
150
|
+
commands: CommandRegistry,
|
|
151
|
+
recents: OmniboxRecents | null,
|
|
152
|
+
path: string
|
|
153
|
+
): void {
|
|
154
|
+
void commands
|
|
155
|
+
.execute('docmanager:open', { path })
|
|
156
|
+
.then(() => {
|
|
157
|
+
recents?.touch('file', path);
|
|
158
|
+
})
|
|
159
|
+
.catch(reason => {
|
|
160
|
+
console.error(`xtralab omnibox: failed to open "${path}"`, reason);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Read a command's label, visibility and caption, or `null` when the command
|
|
166
|
+
* is missing, hidden, label-less, or its accessors throw (a command's
|
|
167
|
+
* accessors can assume a context — e.g. an active notebook — the omnibox
|
|
168
|
+
* doesn't provide).
|
|
169
|
+
*/
|
|
170
|
+
function commandDisplay(
|
|
171
|
+
commands: CommandRegistry,
|
|
172
|
+
id: string
|
|
173
|
+
): { label: string; caption: string } | null {
|
|
174
|
+
if (!commands.hasCommand(id)) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const label = commands.label(id);
|
|
179
|
+
if (!label || !commands.isVisible(id)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return { label, caption: commands.caption(id) };
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The recently used rows for the empty term: both kinds interleaved by
|
|
190
|
+
* recency in the unprefixed view, or only the mode's kind under a bare `>` or
|
|
191
|
+
* `/`. Commands that no longer resolve are dropped, as are files missing from
|
|
192
|
+
* the loaded workspace listing (while the listing is still loading — or
|
|
193
|
+
* unavailable — file entries are shown as recorded).
|
|
194
|
+
*/
|
|
195
|
+
function buildRecentItems(
|
|
196
|
+
options: IComputeOptions,
|
|
197
|
+
mode: Mode
|
|
96
198
|
): IOmniboxItem[] {
|
|
199
|
+
const { commands, docRegistry, files, recents } = options;
|
|
200
|
+
if (!recents) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
const kind: RecentKind | undefined =
|
|
204
|
+
mode === 'commands' ? 'command' : mode === 'files' ? 'file' : undefined;
|
|
205
|
+
const knownFiles = files.length > 0 ? new Set(files) : null;
|
|
206
|
+
|
|
207
|
+
const items: IOmniboxItem[] = [];
|
|
208
|
+
for (const entry of recents.entries(kind)) {
|
|
209
|
+
if (entry.kind === 'command') {
|
|
210
|
+
const display = commandDisplay(commands, entry.id);
|
|
211
|
+
if (!display) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
items.push({
|
|
215
|
+
kind: 'command',
|
|
216
|
+
key: `recent:command:${entry.id}`,
|
|
217
|
+
label: display.label,
|
|
218
|
+
matchIndices: [],
|
|
219
|
+
caption: display.caption || undefined,
|
|
220
|
+
execute: () => {
|
|
221
|
+
executeCommand(commands, recents, entry.id);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
} else {
|
|
225
|
+
if (knownFiles && !knownFiles.has(entry.id)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
items.push({
|
|
229
|
+
kind: 'file',
|
|
230
|
+
key: `recent:file:${entry.id}`,
|
|
231
|
+
label: entry.id,
|
|
232
|
+
matchIndices: [],
|
|
233
|
+
icon: fileIconForPath(docRegistry, entry.id),
|
|
234
|
+
execute: () => {
|
|
235
|
+
openFile(commands, recents, entry.id);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return items;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function matchCommands(options: IComputeOptions, term: string): IOmniboxItem[] {
|
|
244
|
+
const { commands, recents } = options;
|
|
97
245
|
const query = term.toLowerCase();
|
|
98
246
|
const scored: Array<{ score: number; item: IOmniboxItem }> = [];
|
|
99
247
|
for (const id of commands.listCommands()) {
|
|
@@ -125,9 +273,7 @@ function matchCommands(
|
|
|
125
273
|
matchIndices: match.indices,
|
|
126
274
|
caption: caption || undefined,
|
|
127
275
|
execute: () => {
|
|
128
|
-
|
|
129
|
-
console.error(`xtralab omnibox: command "${id}" failed`, reason);
|
|
130
|
-
});
|
|
276
|
+
executeCommand(commands, recents, id);
|
|
131
277
|
}
|
|
132
278
|
}
|
|
133
279
|
});
|
|
@@ -136,12 +282,8 @@ function matchCommands(
|
|
|
136
282
|
return scored.slice(0, COMMAND_LIMIT).map(entry => entry.item);
|
|
137
283
|
}
|
|
138
284
|
|
|
139
|
-
function matchFiles(
|
|
140
|
-
commands
|
|
141
|
-
docRegistry: DocumentRegistry,
|
|
142
|
-
files: string[],
|
|
143
|
-
term: string
|
|
144
|
-
): IOmniboxItem[] {
|
|
285
|
+
function matchFiles(options: IComputeOptions, term: string): IOmniboxItem[] {
|
|
286
|
+
const { commands, docRegistry, files, recents } = options;
|
|
145
287
|
const query = term.toLowerCase();
|
|
146
288
|
const scored: Array<{ score: number; path: string; indices: number[] }> = [];
|
|
147
289
|
for (const path of files) {
|
|
@@ -158,9 +300,7 @@ function matchFiles(
|
|
|
158
300
|
matchIndices: indices,
|
|
159
301
|
icon: fileIconForPath(docRegistry, path),
|
|
160
302
|
execute: () => {
|
|
161
|
-
|
|
162
|
-
console.error(`xtralab omnibox: failed to open "${path}"`, reason);
|
|
163
|
-
});
|
|
303
|
+
openFile(commands, recents, path);
|
|
164
304
|
}
|
|
165
305
|
}));
|
|
166
306
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { IStateDB } from '@jupyterlab/statedb';
|
|
2
|
+
import type { PartialJSONObject } from '@lumino/coreutils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kinds of omnibox results whose uses are recorded.
|
|
6
|
+
*/
|
|
7
|
+
export type RecentKind = 'command' | 'file';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A single recorded use: a command that was run or a file that was opened.
|
|
11
|
+
*/
|
|
12
|
+
interface IRecentEntry {
|
|
13
|
+
kind: RecentKind;
|
|
14
|
+
/**
|
|
15
|
+
* The command id, or the workspace-relative file path.
|
|
16
|
+
*/
|
|
17
|
+
id: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* State database key the recents list is persisted under.
|
|
22
|
+
*/
|
|
23
|
+
const STATE_KEY = 'xtralab:omnibox:recents';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A most-recently-used list of omnibox results, generic over the result kind
|
|
27
|
+
* so commands and files (and future kinds) share one store. Entries are kept
|
|
28
|
+
* most-recent-first, capped at `maxItems` per kind, and persisted through the
|
|
29
|
+
* state database so they survive reloads. Without a state database the list
|
|
30
|
+
* still works for the lifetime of the page.
|
|
31
|
+
*/
|
|
32
|
+
export class OmniboxRecents {
|
|
33
|
+
constructor(options: OmniboxRecents.IOptions = {}) {
|
|
34
|
+
this._state = options.state ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maximum number of entries remembered per kind. Lowering it trims the list
|
|
39
|
+
* immediately; `0` disables recents entirely.
|
|
40
|
+
*/
|
|
41
|
+
get maxItems(): number {
|
|
42
|
+
return this._maxItems;
|
|
43
|
+
}
|
|
44
|
+
set maxItems(value: number) {
|
|
45
|
+
const normalized = Math.max(0, Math.floor(value));
|
|
46
|
+
if (normalized === this._maxItems) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this._maxItems = normalized;
|
|
50
|
+
const trimmed = this._trim(this._entries);
|
|
51
|
+
if (trimmed.length !== this._entries.length) {
|
|
52
|
+
this._entries = trimmed;
|
|
53
|
+
this._save();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The recorded entries, most recent first — all of them, or only those of
|
|
59
|
+
* one kind.
|
|
60
|
+
*/
|
|
61
|
+
entries(kind?: RecentKind): IRecentEntry[] {
|
|
62
|
+
return kind
|
|
63
|
+
? this._entries.filter(entry => entry.kind === kind)
|
|
64
|
+
: [...this._entries];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Record a use of `id`, moving it to the front of its kind's list.
|
|
69
|
+
*/
|
|
70
|
+
touch(kind: RecentKind, id: string): void {
|
|
71
|
+
const rest = this._entries.filter(
|
|
72
|
+
entry => !(entry.kind === kind && entry.id === id)
|
|
73
|
+
);
|
|
74
|
+
this._entries = this._trim([{ kind, id }, ...rest]);
|
|
75
|
+
this._save();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load the persisted list. Entries recorded before the load completes stay
|
|
80
|
+
* at the front, ahead of the restored history.
|
|
81
|
+
*/
|
|
82
|
+
async restore(): Promise<void> {
|
|
83
|
+
if (!this._state) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let fetched: unknown;
|
|
87
|
+
try {
|
|
88
|
+
fetched = await this._state.fetch(STATE_KEY);
|
|
89
|
+
} catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const persisted: IRecentEntry[] = Array.isArray(fetched)
|
|
93
|
+
? fetched.filter(
|
|
94
|
+
(entry): entry is IRecentEntry =>
|
|
95
|
+
!!entry &&
|
|
96
|
+
typeof entry === 'object' &&
|
|
97
|
+
((entry as IRecentEntry).kind === 'command' ||
|
|
98
|
+
(entry as IRecentEntry).kind === 'file') &&
|
|
99
|
+
typeof (entry as IRecentEntry).id === 'string'
|
|
100
|
+
)
|
|
101
|
+
: [];
|
|
102
|
+
const merged = [...this._entries];
|
|
103
|
+
for (const entry of persisted) {
|
|
104
|
+
if (
|
|
105
|
+
!merged.some(
|
|
106
|
+
existing => existing.kind === entry.kind && existing.id === entry.id
|
|
107
|
+
)
|
|
108
|
+
) {
|
|
109
|
+
merged.push({ kind: entry.kind, id: entry.id });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this._entries = this._trim(merged);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Keep at most `maxItems` entries of each kind, preserving order.
|
|
117
|
+
*/
|
|
118
|
+
private _trim(entries: IRecentEntry[]): IRecentEntry[] {
|
|
119
|
+
const counts = new Map<RecentKind, number>();
|
|
120
|
+
return entries.filter(entry => {
|
|
121
|
+
const count = counts.get(entry.kind) ?? 0;
|
|
122
|
+
counts.set(entry.kind, count + 1);
|
|
123
|
+
return count < this._maxItems;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private _save(): void {
|
|
128
|
+
if (!this._state) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const value = this._entries.map<PartialJSONObject>(entry => ({
|
|
132
|
+
kind: entry.kind,
|
|
133
|
+
id: entry.id
|
|
134
|
+
}));
|
|
135
|
+
this._state.save(STATE_KEY, value).catch(reason => {
|
|
136
|
+
console.error('xtralab omnibox: failed to save recents', reason);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private _state: IStateDB | null;
|
|
141
|
+
private _entries: IRecentEntry[] = [];
|
|
142
|
+
private _maxItems = 5;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export namespace OmniboxRecents {
|
|
146
|
+
/**
|
|
147
|
+
* Construction options for {@link OmniboxRecents}.
|
|
148
|
+
*/
|
|
149
|
+
export interface IOptions {
|
|
150
|
+
/**
|
|
151
|
+
* The state database to persist through, if available.
|
|
152
|
+
*/
|
|
153
|
+
state?: IStateDB | null;
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/omnibox/tokens.ts
CHANGED
|
@@ -7,9 +7,13 @@ import { Token } from '@lumino/coreutils';
|
|
|
7
7
|
* by the top-bar command bar, which opens it on click.
|
|
8
8
|
*/
|
|
9
9
|
export interface IOmnibox {
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Open the overlay, optionally seeding the input with `query`.
|
|
12
|
+
*/
|
|
11
13
|
open(query?: string): void;
|
|
12
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Close the overlay if it is open.
|
|
16
|
+
*/
|
|
13
17
|
close(): void;
|
|
14
18
|
}
|
|
15
19
|
|
package/src/omnibox/widget.tsx
CHANGED
|
@@ -8,11 +8,16 @@ import type { IAgent } from '../launcher/agents';
|
|
|
8
8
|
|
|
9
9
|
import { computeSections, IOmniboxItem } from './model';
|
|
10
10
|
import { loadWorkspaceFiles } from './files';
|
|
11
|
+
import type { OmniboxRecents } from './recents';
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* DOM id of the results listbox, referenced by the input's `aria-controls`.
|
|
15
|
+
*/
|
|
13
16
|
const LIST_ID = 'jp-xtralab-Omnibox-list';
|
|
14
17
|
|
|
15
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Stable DOM id for the result row at `index`, for `aria-activedescendant`.
|
|
20
|
+
*/
|
|
16
21
|
function optionId(index: number): string {
|
|
17
22
|
return `jp-xtralab-Omnibox-option-${index}`;
|
|
18
23
|
}
|
|
@@ -67,6 +72,7 @@ function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
|
|
|
67
72
|
agents,
|
|
68
73
|
placeholder,
|
|
69
74
|
initialQuery,
|
|
75
|
+
recents,
|
|
70
76
|
trans,
|
|
71
77
|
onClose
|
|
72
78
|
} = props;
|
|
@@ -94,11 +100,24 @@ function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
|
|
|
94
100
|
|
|
95
101
|
const sections = React.useMemo(
|
|
96
102
|
() =>
|
|
97
|
-
computeSections({
|
|
98
|
-
|
|
103
|
+
computeSections({
|
|
104
|
+
query,
|
|
105
|
+
commands,
|
|
106
|
+
docRegistry,
|
|
107
|
+
agents,
|
|
108
|
+
files,
|
|
109
|
+
recents,
|
|
110
|
+
trans
|
|
111
|
+
}),
|
|
112
|
+
[query, commands, docRegistry, agents, files, recents, trans]
|
|
99
113
|
);
|
|
100
114
|
const flat = React.useMemo(
|
|
101
|
-
() => [
|
|
115
|
+
() => [
|
|
116
|
+
...sections.recent,
|
|
117
|
+
...sections.commands,
|
|
118
|
+
...sections.files,
|
|
119
|
+
...sections.agents
|
|
120
|
+
],
|
|
102
121
|
[sections]
|
|
103
122
|
);
|
|
104
123
|
const hasResults = flat.length > 0;
|
|
@@ -212,7 +231,8 @@ function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
|
|
|
212
231
|
);
|
|
213
232
|
};
|
|
214
233
|
|
|
215
|
-
const
|
|
234
|
+
const commandsStart = sections.recent.length;
|
|
235
|
+
const filesStart = commandsStart + sections.commands.length;
|
|
216
236
|
const agentsStart = filesStart + sections.files.length;
|
|
217
237
|
|
|
218
238
|
return (
|
|
@@ -255,7 +275,12 @@ function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
|
|
|
255
275
|
>
|
|
256
276
|
{hasResults ? (
|
|
257
277
|
<>
|
|
258
|
-
{renderSection(trans.__('
|
|
278
|
+
{renderSection(trans.__('Recently used'), sections.recent, 0)}
|
|
279
|
+
{renderSection(
|
|
280
|
+
trans.__('Commands'),
|
|
281
|
+
sections.commands,
|
|
282
|
+
commandsStart
|
|
283
|
+
)}
|
|
259
284
|
{renderSection(trans.__('Files'), sections.files, filesStart)}
|
|
260
285
|
{renderSection(
|
|
261
286
|
trans.__('Ask an agent'),
|
|
@@ -304,19 +329,35 @@ export class OmniboxWidget extends ReactWidget {
|
|
|
304
329
|
* A namespace for `OmniboxWidget` statics.
|
|
305
330
|
*/
|
|
306
331
|
export namespace OmniboxWidget {
|
|
307
|
-
/**
|
|
332
|
+
/**
|
|
333
|
+
* Construction options for {@link OmniboxWidget}.
|
|
334
|
+
*/
|
|
308
335
|
export interface IOptions {
|
|
309
336
|
commands: CommandRegistry;
|
|
310
337
|
docRegistry: DocumentRegistry;
|
|
311
|
-
/**
|
|
338
|
+
/**
|
|
339
|
+
* Snapshot of the available agents, read once when the overlay opens.
|
|
340
|
+
*/
|
|
312
341
|
agents: IAgent[];
|
|
313
|
-
/**
|
|
342
|
+
/**
|
|
343
|
+
* Placeholder text for the input.
|
|
344
|
+
*/
|
|
314
345
|
placeholder: string;
|
|
315
|
-
/**
|
|
346
|
+
/**
|
|
347
|
+
* Seed text for the input.
|
|
348
|
+
*/
|
|
316
349
|
initialQuery: string;
|
|
317
|
-
/**
|
|
350
|
+
/**
|
|
351
|
+
* Recently-used tracker; `null` disables the recent rows and recording.
|
|
352
|
+
*/
|
|
353
|
+
recents: OmniboxRecents | null;
|
|
354
|
+
/**
|
|
355
|
+
* Translation bundle for the overlay's own labels.
|
|
356
|
+
*/
|
|
318
357
|
trans: TranslationBundle;
|
|
319
|
-
/**
|
|
358
|
+
/**
|
|
359
|
+
* Dismiss the overlay (the plugin disposes the widget).
|
|
360
|
+
*/
|
|
320
361
|
onClose: () => void;
|
|
321
362
|
}
|
|
322
363
|
}
|