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.
Files changed (162) hide show
  1. package/README.md +37 -229
  2. package/lib/agentSessions.d.ts +6 -2
  3. package/lib/askAgent/editorSelection.d.ts +53 -0
  4. package/lib/askAgent/editorSelection.js +95 -0
  5. package/lib/askAgent/icons.d.ts +7 -0
  6. package/lib/askAgent/icons.js +15 -0
  7. package/lib/askAgent/index.d.ts +32 -0
  8. package/lib/askAgent/index.js +741 -0
  9. package/lib/askAgent/popup.d.ts +83 -0
  10. package/lib/askAgent/popup.js +221 -0
  11. package/lib/askAgent/prompt.d.ts +24 -0
  12. package/lib/askAgent/prompt.js +116 -0
  13. package/lib/askAgent/queue.d.ts +93 -0
  14. package/lib/askAgent/queue.js +207 -0
  15. package/lib/askAgent/queuePanel.d.ts +76 -0
  16. package/lib/askAgent/queuePanel.js +188 -0
  17. package/lib/askAgent/targetPicker.d.ts +58 -0
  18. package/lib/askAgent/targetPicker.js +74 -0
  19. package/lib/askAgent/tokens.d.ts +124 -0
  20. package/lib/askAgent/tokens.js +13 -0
  21. package/lib/commandBar/index.d.ts +12 -8
  22. package/lib/commandBar/index.js +58 -41
  23. package/lib/editorBreadcrumbs/index.js +9 -4
  24. package/lib/editorBreadcrumbs/widget.d.ts +7 -7
  25. package/lib/editorBreadcrumbs/widget.js +6 -8
  26. package/lib/fileBrowser/commands.d.ts +2 -8
  27. package/lib/fileBrowser/commands.js +39 -30
  28. package/lib/fileBrowser/contents.d.ts +8 -3
  29. package/lib/fileBrowser/fileBrowser.d.ts +2 -1
  30. package/lib/fileBrowser/widget.d.ts +5 -9
  31. package/lib/fileBrowser/widget.js +1 -1
  32. package/lib/fileTypeIcons/index.js +5 -2
  33. package/lib/git/api.d.ts +0 -26
  34. package/lib/git/api.js +2 -44
  35. package/lib/git/askRequest.d.ts +23 -0
  36. package/lib/git/askRequest.js +90 -0
  37. package/lib/git/commands.d.ts +14 -2
  38. package/lib/git/commands.js +16 -13
  39. package/lib/git/diffModel.d.ts +1 -9
  40. package/lib/git/diffModel.js +1 -1
  41. package/lib/git/diffProvider.js +8 -3
  42. package/lib/git/diffSurface.d.ts +19 -2
  43. package/lib/git/diffSurface.js +64 -18
  44. package/lib/git/diffTheme.d.ts +0 -4
  45. package/lib/git/diffTheme.js +1 -1
  46. package/lib/git/diffWidget.d.ts +9 -0
  47. package/lib/git/diffWidget.js +38 -10
  48. package/lib/git/imageDiff.d.ts +17 -4
  49. package/lib/git/imageDiff.js +90 -30
  50. package/lib/git/index.js +7 -2
  51. package/lib/git/notebookDiff.d.ts +10 -67
  52. package/lib/git/notebookDiff.js +62 -69
  53. package/lib/git/tokens.d.ts +5 -2
  54. package/lib/highlight/index.js +12 -8
  55. package/lib/index.js +2 -0
  56. package/lib/launcher/agents.d.ts +11 -1
  57. package/lib/launcher/agents.js +26 -8
  58. package/lib/launcher/commands.d.ts +6 -2
  59. package/lib/launcher/dashboard.d.ts +8 -1
  60. package/lib/launcher/dashboard.js +39 -33
  61. package/lib/launcher/editorRegistry.js +5 -3
  62. package/lib/launcher/editors.d.ts +24 -7
  63. package/lib/launcher/editors.js +14 -5
  64. package/lib/launcher/icons.d.ts +0 -37
  65. package/lib/launcher/icons.js +11 -12
  66. package/lib/launcher/index.js +5 -2
  67. package/lib/launcher/invocation.d.ts +0 -15
  68. package/lib/launcher/invocation.js +1 -1
  69. package/lib/launcher/schemaDefaults.d.ts +3 -1
  70. package/lib/launcher/schemaDefaults.js +3 -1
  71. package/lib/mimeData.d.ts +7 -0
  72. package/lib/mimeData.js +16 -0
  73. package/lib/omnibox/index.d.ts +4 -0
  74. package/lib/omnibox/index.js +43 -2
  75. package/lib/omnibox/model.d.ts +43 -14
  76. package/lib/omnibox/model.js +117 -14
  77. package/lib/omnibox/recents.d.ts +65 -0
  78. package/lib/omnibox/recents.js +109 -0
  79. package/lib/omnibox/tokens.d.ts +6 -2
  80. package/lib/omnibox/widget.d.ts +23 -6
  81. package/lib/omnibox/widget.js +26 -7
  82. package/lib/showOutput/index.js +15 -18
  83. package/lib/terminals/agentTerminals.d.ts +63 -0
  84. package/lib/terminals/agentTerminals.js +157 -0
  85. package/lib/terminals/index.d.ts +7 -1
  86. package/lib/terminals/index.js +50 -30
  87. package/lib/terminals/model.d.ts +11 -2
  88. package/lib/terminals/model.js +27 -8
  89. package/lib/terminals/tokens.d.ts +66 -0
  90. package/lib/terminals/tokens.js +7 -0
  91. package/lib/terminals/widget.d.ts +3 -6
  92. package/lib/terminals/widget.js +1 -1
  93. package/lib/topBar/index.js +3 -3
  94. package/lib/walkthrough/index.js +14 -3
  95. package/lib/walkthrough/panel.d.ts +12 -4
  96. package/lib/walkthrough/panel.js +9 -3
  97. package/package.json +49 -51
  98. package/schema/command-bar.json +10 -0
  99. package/schema/launcher.json +4 -4
  100. package/schema/omnibox.json +15 -0
  101. package/src/agentSessions.ts +6 -2
  102. package/src/askAgent/editorSelection.ts +138 -0
  103. package/src/askAgent/icons.ts +16 -0
  104. package/src/askAgent/index.ts +915 -0
  105. package/src/askAgent/popup.tsx +407 -0
  106. package/src/askAgent/prompt.ts +144 -0
  107. package/src/askAgent/queue.ts +269 -0
  108. package/src/askAgent/queuePanel.tsx +418 -0
  109. package/src/askAgent/targetPicker.tsx +209 -0
  110. package/src/askAgent/tokens.ts +139 -0
  111. package/src/commandBar/index.ts +75 -49
  112. package/src/editorBreadcrumbs/index.ts +14 -4
  113. package/src/editorBreadcrumbs/widget.ts +11 -9
  114. package/src/fileBrowser/commands.ts +45 -31
  115. package/src/fileBrowser/contents.ts +7 -3
  116. package/src/fileBrowser/fileBrowser.tsx +1 -1
  117. package/src/fileBrowser/widget.tsx +5 -3
  118. package/src/fileTypeIcons/index.ts +6 -3
  119. package/src/git/api.ts +2 -61
  120. package/src/git/askRequest.ts +113 -0
  121. package/src/git/commands.ts +41 -14
  122. package/src/git/diffModel.ts +1 -1
  123. package/src/git/diffProvider.tsx +11 -3
  124. package/src/git/diffSurface.tsx +114 -22
  125. package/src/git/diffTheme.ts +1 -1
  126. package/src/git/diffWidget.tsx +54 -7
  127. package/src/git/imageDiff.tsx +169 -58
  128. package/src/git/index.ts +10 -2
  129. package/src/git/notebookDiff.tsx +91 -57
  130. package/src/git/tokens.ts +4 -2
  131. package/src/highlight/index.ts +12 -8
  132. package/src/index.ts +2 -0
  133. package/src/launcher/agents.ts +31 -7
  134. package/src/launcher/commands.ts +6 -2
  135. package/src/launcher/dashboard.tsx +86 -38
  136. package/src/launcher/editorRegistry.ts +10 -4
  137. package/src/launcher/editors.ts +34 -9
  138. package/src/launcher/icons.ts +11 -12
  139. package/src/launcher/index.ts +5 -2
  140. package/src/launcher/invocation.ts +1 -1
  141. package/src/launcher/schemaDefaults.ts +3 -1
  142. package/src/mimeData.ts +20 -0
  143. package/src/omnibox/index.ts +48 -1
  144. package/src/omnibox/model.ts +173 -33
  145. package/src/omnibox/recents.ts +155 -0
  146. package/src/omnibox/tokens.ts +6 -2
  147. package/src/omnibox/widget.tsx +54 -13
  148. package/src/showOutput/index.ts +20 -23
  149. package/src/terminals/agentTerminals.ts +238 -0
  150. package/src/terminals/index.ts +54 -30
  151. package/src/terminals/model.ts +28 -7
  152. package/src/terminals/tokens.ts +77 -0
  153. package/src/terminals/widget.tsx +4 -2
  154. package/src/topBar/index.ts +12 -6
  155. package/src/walkthrough/index.ts +19 -4
  156. package/src/walkthrough/panel.ts +15 -5
  157. package/style/askAgent.css +544 -0
  158. package/style/commandBar.css +12 -33
  159. package/style/git.css +72 -31
  160. package/style/index.css +1 -0
  161. package/style/index.js +2 -0
  162. package/style/topBar.css +4 -4
@@ -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: [IAgentRegistry, ICommandPalette, ITranslator],
24
- activate: (app, agentRegistry, palette, translator) => {
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
  });
@@ -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
- /** The source a result row came from, used to group rows under a header. */
7
- export type OmniboxItemKind = 'command' | 'file' | 'agent';
8
- /** A single result row, with the action it runs when chosen. */
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
- /** Stable React key. */
16
+ /**
17
+ * Stable React key.
18
+ */
12
19
  key: string;
13
- /** Primary text shown on the row. */
20
+ /**
21
+ * Primary text shown on the row.
22
+ */
14
23
  label: string;
15
- /** Indices of matched characters in `label`, for highlighting. May be empty. */
24
+ /**
25
+ * Indices of matched characters in `label`, for highlighting. May be empty.
26
+ */
16
27
  matchIndices: readonly number[];
17
- /** Optional secondary text shown trailing the label. */
28
+ /**
29
+ * Optional secondary text shown trailing the label.
30
+ */
18
31
  caption?: string;
19
- /** Optional leading icon. */
32
+ /**
33
+ * Optional leading icon.
34
+ */
20
35
  icon?: LabIcon;
21
- /** Run the row's action. */
36
+ /**
37
+ * Run the row's action.
38
+ */
22
39
  execute: () => void;
23
40
  }
24
- /** Results grouped by source; each group is already capped and sorted. */
25
- export interface IOmniboxSections {
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
- export interface IComputeOptions {
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 no rows (the widget
43
- * shows a hint instead).
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 {};
@@ -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 no rows (the widget
18
- * shows a hint instead).
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, docRegistry, agents, files, trans } = options;
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 { commands: [], files: [], agents: [] };
35
+ return {
36
+ recent: buildRecentItems(options, mode),
37
+ commands: [],
38
+ files: [],
39
+ agents: []
40
+ };
35
41
  }
36
42
  return {
37
- commands: mode === 'files' ? [] : matchCommands(commands, term),
38
- files: mode === 'commands' ? [] : matchFiles(commands, docRegistry, files, term),
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
- function matchCommands(commands, term) {
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
- void commands.execute(id).catch(reason => {
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(commands, docRegistry, files, term) {
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
- void commands.execute('docmanager:open', { path }).catch(reason => {
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
+ }
@@ -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
- /** Open the overlay, optionally seeding the input with `query`. */
9
+ /**
10
+ * Open the overlay, optionally seeding the input with `query`.
11
+ */
10
12
  open(query?: string): void;
11
- /** Close the overlay if it is open. */
13
+ /**
14
+ * Close the overlay if it is open.
15
+ */
12
16
  close(): void;
13
17
  }
14
18
  /**
@@ -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
- /** Construction options for {@link OmniboxWidget}. */
22
+ /**
23
+ * Construction options for {@link OmniboxWidget}.
24
+ */
22
25
  interface IOptions {
23
26
  commands: CommandRegistry;
24
27
  docRegistry: DocumentRegistry;
25
- /** Snapshot of the available agents, read once when the overlay opens. */
28
+ /**
29
+ * Snapshot of the available agents, read once when the overlay opens.
30
+ */
26
31
  agents: IAgent[];
27
- /** Placeholder text for the input. */
32
+ /**
33
+ * Placeholder text for the input.
34
+ */
28
35
  placeholder: string;
29
- /** Seed text for the input. */
36
+ /**
37
+ * Seed text for the input.
38
+ */
30
39
  initialQuery: string;
31
- /** Translation bundle for the overlay's own labels. */
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
- /** Dismiss the overlay (the plugin disposes the widget). */
48
+ /**
49
+ * Dismiss the overlay (the plugin disposes the widget).
50
+ */
34
51
  onClose: () => void;
35
52
  }
36
53
  }
@@ -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
- /** DOM id of the results listbox, referenced by the input's `aria-controls`. */
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
- /** Stable DOM id for the result row at `index`, for `aria-activedescendant`. */
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({ query, commands, docRegistry, agents, files, trans }), [query, commands, docRegistry, agents, files, trans]);
60
- const flat = React.useMemo(() => [...sections.commands, ...sections.files, ...sections.agents], [sections]);
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 filesStart = sections.commands.length;
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.__('Commands'), sections.commands, 0),
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.')