xtralab 0.11.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +38 -231
  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.js +3 -4
  57. package/lib/launcher/commands.d.ts +6 -2
  58. package/lib/launcher/dashboard.d.ts +8 -1
  59. package/lib/launcher/dashboard.js +39 -33
  60. package/lib/launcher/editorRegistry.js +5 -3
  61. package/lib/launcher/editors.d.ts +15 -5
  62. package/lib/launcher/icons.d.ts +0 -37
  63. package/lib/launcher/icons.js +11 -12
  64. package/lib/launcher/index.js +2 -1
  65. package/lib/launcher/invocation.d.ts +0 -15
  66. package/lib/launcher/invocation.js +1 -1
  67. package/lib/launcher/schemaDefaults.d.ts +3 -1
  68. package/lib/launcher/schemaDefaults.js +3 -1
  69. package/lib/mimeData.d.ts +7 -0
  70. package/lib/mimeData.js +16 -0
  71. package/lib/omnibox/index.d.ts +4 -0
  72. package/lib/omnibox/index.js +43 -2
  73. package/lib/omnibox/model.d.ts +43 -14
  74. package/lib/omnibox/model.js +117 -14
  75. package/lib/omnibox/recents.d.ts +65 -0
  76. package/lib/omnibox/recents.js +109 -0
  77. package/lib/omnibox/tokens.d.ts +6 -2
  78. package/lib/omnibox/widget.d.ts +23 -6
  79. package/lib/omnibox/widget.js +26 -7
  80. package/lib/showOutput/index.js +15 -18
  81. package/lib/terminals/agentTerminals.d.ts +63 -0
  82. package/lib/terminals/agentTerminals.js +157 -0
  83. package/lib/terminals/index.d.ts +7 -1
  84. package/lib/terminals/index.js +50 -30
  85. package/lib/terminals/model.d.ts +11 -2
  86. package/lib/terminals/model.js +27 -8
  87. package/lib/terminals/tokens.d.ts +66 -0
  88. package/lib/terminals/tokens.js +7 -0
  89. package/lib/terminals/widget.d.ts +3 -6
  90. package/lib/terminals/widget.js +1 -1
  91. package/lib/topBar/index.js +3 -3
  92. package/lib/walkthrough/index.js +14 -3
  93. package/lib/walkthrough/panel.d.ts +12 -4
  94. package/lib/walkthrough/panel.js +9 -3
  95. package/package.json +49 -51
  96. package/schema/command-bar.json +10 -0
  97. package/schema/omnibox.json +15 -0
  98. package/src/agentSessions.ts +6 -2
  99. package/src/askAgent/editorSelection.ts +138 -0
  100. package/src/askAgent/icons.ts +16 -0
  101. package/src/askAgent/index.ts +915 -0
  102. package/src/askAgent/popup.tsx +407 -0
  103. package/src/askAgent/prompt.ts +144 -0
  104. package/src/askAgent/queue.ts +269 -0
  105. package/src/askAgent/queuePanel.tsx +418 -0
  106. package/src/askAgent/targetPicker.tsx +209 -0
  107. package/src/askAgent/tokens.ts +139 -0
  108. package/src/commandBar/index.ts +75 -49
  109. package/src/editorBreadcrumbs/index.ts +14 -4
  110. package/src/editorBreadcrumbs/widget.ts +11 -9
  111. package/src/fileBrowser/commands.ts +45 -31
  112. package/src/fileBrowser/contents.ts +7 -3
  113. package/src/fileBrowser/fileBrowser.tsx +1 -1
  114. package/src/fileBrowser/widget.tsx +5 -3
  115. package/src/fileTypeIcons/index.ts +6 -3
  116. package/src/git/api.ts +2 -61
  117. package/src/git/askRequest.ts +113 -0
  118. package/src/git/commands.ts +41 -14
  119. package/src/git/diffModel.ts +1 -1
  120. package/src/git/diffProvider.tsx +11 -3
  121. package/src/git/diffSurface.tsx +114 -22
  122. package/src/git/diffTheme.ts +1 -1
  123. package/src/git/diffWidget.tsx +54 -7
  124. package/src/git/imageDiff.tsx +169 -58
  125. package/src/git/index.ts +10 -2
  126. package/src/git/notebookDiff.tsx +91 -57
  127. package/src/git/tokens.ts +4 -2
  128. package/src/highlight/index.ts +12 -8
  129. package/src/index.ts +2 -0
  130. package/src/launcher/agents.ts +3 -4
  131. package/src/launcher/commands.ts +6 -2
  132. package/src/launcher/dashboard.tsx +86 -38
  133. package/src/launcher/editorRegistry.ts +10 -4
  134. package/src/launcher/editors.ts +15 -5
  135. package/src/launcher/icons.ts +11 -12
  136. package/src/launcher/index.ts +2 -1
  137. package/src/launcher/invocation.ts +1 -1
  138. package/src/launcher/schemaDefaults.ts +3 -1
  139. package/src/mimeData.ts +20 -0
  140. package/src/omnibox/index.ts +48 -1
  141. package/src/omnibox/model.ts +173 -33
  142. package/src/omnibox/recents.ts +155 -0
  143. package/src/omnibox/tokens.ts +6 -2
  144. package/src/omnibox/widget.tsx +54 -13
  145. package/src/showOutput/index.ts +20 -23
  146. package/src/terminals/agentTerminals.ts +238 -0
  147. package/src/terminals/index.ts +54 -30
  148. package/src/terminals/model.ts +28 -7
  149. package/src/terminals/tokens.ts +77 -0
  150. package/src/terminals/widget.tsx +4 -2
  151. package/src/topBar/index.ts +12 -6
  152. package/src/walkthrough/index.ts +19 -4
  153. package/src/walkthrough/panel.ts +15 -5
  154. package/style/askAgent.css +544 -0
  155. package/style/commandBar.css +12 -33
  156. package/style/git.css +72 -31
  157. package/style/index.css +1 -0
  158. package/style/index.js +2 -0
  159. package/style/topBar.css +4 -4
@@ -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
- /** The source a result row came from, used to group rows under a header. */
11
- export type OmniboxItemKind = 'command' | 'file' | 'agent';
10
+ import type { OmniboxRecents, RecentKind } from './recents';
12
11
 
13
- /** A single result row, with the action it runs when chosen. */
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
- /** Stable React key. */
22
+ /**
23
+ * Stable React key.
24
+ */
17
25
  key: string;
18
- /** Primary text shown on the row. */
26
+ /**
27
+ * Primary text shown on the row.
28
+ */
19
29
  label: string;
20
- /** Indices of matched characters in `label`, for highlighting. May be empty. */
30
+ /**
31
+ * Indices of matched characters in `label`, for highlighting. May be empty.
32
+ */
21
33
  matchIndices: readonly number[];
22
- /** Optional secondary text shown trailing the label. */
34
+ /**
35
+ * Optional secondary text shown trailing the label.
36
+ */
23
37
  caption?: string;
24
- /** Optional leading icon. */
38
+ /**
39
+ * Optional leading icon.
40
+ */
25
41
  icon?: LabIcon;
26
- /** Run the row's action. */
42
+ /**
43
+ * Run the row's action.
44
+ */
27
45
  execute: () => void;
28
46
  }
29
47
 
30
- /** Results grouped by source; each group is already capped and sorted. */
31
- export interface IOmniboxSections {
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
- export interface IComputeOptions {
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 no rows (the widget
64
- * shows a hint instead).
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, docRegistry, agents, files, trans } = options;
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 { commands: [], files: [], agents: [] };
110
+ return {
111
+ recent: buildRecentItems(options, mode),
112
+ commands: [],
113
+ files: [],
114
+ agents: []
115
+ };
82
116
  }
83
117
 
84
118
  return {
85
- commands: mode === 'files' ? [] : matchCommands(commands, term),
86
- files:
87
- mode === 'commands' ? [] : matchFiles(commands, docRegistry, files, term),
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
- function matchCommands(
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
- term: string
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
- void commands.execute(id).catch(reason => {
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: CommandRegistry,
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
- void commands.execute('docmanager:open', { path }).catch(reason => {
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
+ }
@@ -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
- /** Open the overlay, optionally seeding the input with `query`. */
10
+ /**
11
+ * Open the overlay, optionally seeding the input with `query`.
12
+ */
11
13
  open(query?: string): void;
12
- /** Close the overlay if it is open. */
14
+ /**
15
+ * Close the overlay if it is open.
16
+ */
13
17
  close(): void;
14
18
  }
15
19
 
@@ -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
- /** DOM id of the results listbox, referenced by the input's `aria-controls`. */
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
- /** Stable DOM id for the result row at `index`, for `aria-activedescendant`. */
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({ query, commands, docRegistry, agents, files, trans }),
98
- [query, commands, docRegistry, agents, files, trans]
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
- () => [...sections.commands, ...sections.files, ...sections.agents],
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 filesStart = sections.commands.length;
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.__('Commands'), sections.commands, 0)}
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
- /** Construction options for {@link OmniboxWidget}. */
332
+ /**
333
+ * Construction options for {@link OmniboxWidget}.
334
+ */
308
335
  export interface IOptions {
309
336
  commands: CommandRegistry;
310
337
  docRegistry: DocumentRegistry;
311
- /** Snapshot of the available agents, read once when the overlay opens. */
338
+ /**
339
+ * Snapshot of the available agents, read once when the overlay opens.
340
+ */
312
341
  agents: IAgent[];
313
- /** Placeholder text for the input. */
342
+ /**
343
+ * Placeholder text for the input.
344
+ */
314
345
  placeholder: string;
315
- /** Seed text for the input. */
346
+ /**
347
+ * Seed text for the input.
348
+ */
316
349
  initialQuery: string;
317
- /** Translation bundle for the overlay's own labels. */
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
- /** Dismiss the overlay (the plugin disposes the widget). */
358
+ /**
359
+ * Dismiss the overlay (the plugin disposes the widget).
360
+ */
320
361
  onClose: () => void;
321
362
  }
322
363
  }
@@ -6,32 +6,16 @@ import {
6
6
  import { ICommandPalette } from '@jupyterlab/apputils';
7
7
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
8
8
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
9
- import type { ReadonlyPartialJSONValue } from '@lumino/coreutils';
10
9
  import { Panel } from '@lumino/widgets';
11
10
 
12
- const PLUGIN_ID = 'xtralab:show-output';
11
+ import { coerceData } from '../mimeData';
13
12
 
14
- /** Render arbitrary rich content into a panel, with no notebook and no kernel. */
15
- const SHOW_COMMAND = 'xtralab:show';
13
+ const PLUGIN_ID = 'xtralab:show-output';
16
14
 
17
15
  /**
18
- * Parse a JSON-typed value that may have arrived as a string. Agents and the
19
- * MCP bridge sometimes serialize a spec (e.g. a Vega-Lite object) to a string;
20
- * for `application/...json` MIME types we accept either and parse the string.
16
+ * Render arbitrary rich content into a panel, with no notebook and no kernel.
21
17
  */
22
- function coerceData(
23
- mimeType: string,
24
- data: ReadonlyPartialJSONValue
25
- ): ReadonlyPartialJSONValue {
26
- if (typeof data === 'string' && /\+?json$/.test(mimeType)) {
27
- try {
28
- return JSON.parse(data) as ReadonlyPartialJSONValue;
29
- } catch {
30
- return data;
31
- }
32
- }
33
- return data;
34
- }
18
+ const SHOW_COMMAND = 'xtralab:show';
35
19
 
36
20
  /**
37
21
  * Contribute `xtralab:show`: render a single MIME bundle into a main-area
@@ -121,8 +105,10 @@ const plugin: JupyterFrontEndPlugin<void> = {
121
105
  : trans.__('Output');
122
106
  const id = typeof args['id'] === 'string' ? args['id'] : 'default';
123
107
  const area = args['area'] === 'main' ? 'main' : 'right';
124
- const mode =
125
- typeof args['mode'] === 'string' ? args['mode'] : 'split-right';
108
+ const mode: DocumentMode =
109
+ typeof args['mode'] === 'string' && isDocumentMode(args['mode'])
110
+ ? args['mode']
111
+ : 'split-right';
126
112
 
127
113
  const bundle = { [mimeType]: data };
128
114
  const chosen = rendermime.preferredMimeType(bundle, 'any');
@@ -161,7 +147,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
161
147
  // only split the document area when `area` is "main".
162
148
  if (area === 'main') {
163
149
  labShell.add(widget, 'main', {
164
- mode: mode as DocumentMode,
150
+ mode,
165
151
  activate: true
166
152
  });
167
153
  } else {
@@ -196,4 +182,15 @@ type DocumentMode =
196
182
  | 'tab-before'
197
183
  | 'tab-after';
198
184
 
185
+ function isDocumentMode(value: string): value is DocumentMode {
186
+ return (
187
+ value === 'split-top' ||
188
+ value === 'split-left' ||
189
+ value === 'split-right' ||
190
+ value === 'split-bottom' ||
191
+ value === 'tab-before' ||
192
+ value === 'tab-after'
193
+ );
194
+ }
195
+
199
196
  export default plugin;