xtralab 0.14.0 → 0.15.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 (52) hide show
  1. package/lib/customPanel/index.d.ts +7 -0
  2. package/lib/customPanel/index.js +74 -0
  3. package/lib/fileBrowser/commands.js +13 -1
  4. package/lib/fileBrowser/contents.d.ts +11 -0
  5. package/lib/fileBrowser/contents.js +28 -0
  6. package/lib/fileBrowser/dragAndDrop.d.ts +66 -0
  7. package/lib/fileBrowser/dragAndDrop.js +115 -0
  8. package/lib/fileBrowser/fileBrowser.d.ts +2 -0
  9. package/lib/fileBrowser/fileBrowser.js +151 -197
  10. package/lib/fileBrowser/index.js +39 -6
  11. package/lib/fileBrowser/widget.d.ts +44 -5
  12. package/lib/fileBrowser/widget.js +153 -14
  13. package/lib/index.js +4 -0
  14. package/lib/launcher/agents.d.ts +1 -1
  15. package/lib/launcher/agents.js +15 -2
  16. package/lib/launcher/icons.js +15 -4
  17. package/lib/launcher/schemaDefaults.d.ts +2 -9
  18. package/lib/launcher/schemaDefaults.js +37 -13
  19. package/lib/menuBar/icons.d.ts +6 -0
  20. package/lib/menuBar/icons.js +13 -0
  21. package/lib/menuBar/index.d.ts +15 -0
  22. package/lib/menuBar/index.js +164 -0
  23. package/lib/sidebar/index.d.ts +7 -13
  24. package/lib/sidebar/index.js +80 -46
  25. package/lib/terminals/index.d.ts +5 -3
  26. package/lib/terminals/index.js +17 -6
  27. package/lib/terminals/widget.d.ts +29 -25
  28. package/lib/terminals/widget.js +150 -65
  29. package/package.json +1 -1
  30. package/schema/launcher.json +2 -2
  31. package/schema/menu-bar.json +37 -0
  32. package/schema/sidebar.json +47 -7
  33. package/src/customPanel/index.ts +104 -0
  34. package/src/fileBrowser/commands.ts +14 -1
  35. package/src/fileBrowser/contents.ts +30 -0
  36. package/src/fileBrowser/dragAndDrop.ts +178 -0
  37. package/src/fileBrowser/fileBrowser.tsx +183 -225
  38. package/src/fileBrowser/index.ts +41 -6
  39. package/src/fileBrowser/widget.tsx +195 -14
  40. package/src/index.ts +4 -0
  41. package/src/launcher/agents.ts +16 -3
  42. package/src/launcher/icons.ts +16 -4
  43. package/src/launcher/schemaDefaults.ts +43 -13
  44. package/src/menuBar/icons.ts +14 -0
  45. package/src/menuBar/index.ts +195 -0
  46. package/src/sidebar/index.ts +95 -52
  47. package/src/terminals/index.ts +24 -6
  48. package/src/terminals/widget.tsx +186 -87
  49. package/style/base.css +22 -34
  50. package/style/customPanel.css +9 -0
  51. package/style/index.js +1 -0
  52. package/style/terminals.css +19 -77
@@ -1,10 +1,21 @@
1
1
  import * as React from 'react';
2
2
 
3
+ import {
4
+ IMovableSectionDestination,
5
+ IMovableSectionSource,
6
+ ISectionEntry
7
+ } from '@jupyterlab/apputils';
3
8
  import { IDocumentManager } from '@jupyterlab/docmanager';
4
9
  import { Contents } from '@jupyterlab/services';
5
- import { ReactWidget, Toolbar } from '@jupyterlab/ui-components';
10
+ import { ITranslator } from '@jupyterlab/translation';
11
+ import {
12
+ AccordionToolbar,
13
+ PanelWithToolbar,
14
+ ReactWidget,
15
+ Toolbar
16
+ } from '@jupyterlab/ui-components';
6
17
  import { Signal, ISignal } from '@lumino/signaling';
7
- import { PanelLayout, Widget } from '@lumino/widgets';
18
+ import { AccordionPanel, PanelLayout, Widget } from '@lumino/widgets';
8
19
 
9
20
  import { FileBrowserComponent } from './fileBrowser';
10
21
  import { xtralabFileBrowserIcon } from './icons';
@@ -12,20 +23,30 @@ import { xtralabFileBrowserIcon } from './icons';
12
23
  export const FILE_BROWSER_ID = 'xtralab:file-browser';
13
24
 
14
25
  /**
15
- * The CSS class added to the xtralab file browser widget. The selectors that
16
- * bind application context menu items hang off this class, so it must remain
17
- * specific enough not to clash with other browsers (such as the default
18
- * `jp-FileBrowser`).
26
+ * Persisted by the movable-sections plugin, so it must stay stable.
27
+ */
28
+ const TREE_SECTION_ID = 'xtralab-file-tree-section';
29
+
30
+ /**
31
+ * Context menu selectors hang off this class, so it sits on the section and
32
+ * keeps matching once the tree moves to another panel.
19
33
  */
20
34
  const FILE_BROWSER_CSS_CLASS = 'jp-xtralab-FileBrowser';
21
35
 
36
+ const PANEL_CSS_CLASS = 'jp-xtralab-FileBrowserPanel';
37
+
22
38
  const TOOLBAR_CSS_CLASS = 'jp-xtralab-FileBrowser-toolbar';
23
39
  const CONTENT_CSS_CLASS = 'jp-xtralab-FileBrowser-content';
24
40
 
41
+ const ACCORDION_CSS_CLASS = 'jp-xtralab-FileBrowser-accordion';
42
+
43
+ const MOVABLE_SECTION_CLASS = 'jp-movable-section';
44
+
25
45
  interface IXtralabFileBrowserOptions {
26
46
  contentsManager: Contents.IManager;
27
47
  docManager: IDocumentManager;
28
48
  onOpenFile?: (serverPath: string) => void;
49
+ translator?: ITranslator;
29
50
  }
30
51
 
31
52
  /**
@@ -155,39 +176,56 @@ export interface IXtralabFileBrowser {
155
176
  * Toggle the filter box above the tree.
156
177
  */
157
178
  toggleFileFilter(): void;
179
+
180
+ readonly sectionNode: HTMLElement;
158
181
  }
159
182
 
160
183
  /**
161
- * Lumino widget that hosts the React-based `@pierre/trees` file browser.
162
- * The widget is laid out with a JupyterLab `Toolbar` on top and the tree
163
- * filling the remaining space.
184
+ * Lumino widget hosting the React-based `@pierre/trees` file browser. The tree
185
+ * and its toolbar form one movable "Files" section, and sections from other
186
+ * panels can be moved in below it.
164
187
  */
165
- export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
188
+ export class XtralabFileBrowser
189
+ extends Widget
190
+ implements
191
+ IXtralabFileBrowser,
192
+ IMovableSectionSource,
193
+ IMovableSectionDestination
194
+ {
166
195
  constructor(options: IXtralabFileBrowserOptions) {
167
196
  super();
168
- const layout = (this.layout = new PanelLayout());
197
+ const layout = (this._panelLayout = this.layout = new PanelLayout());
169
198
 
170
199
  this._contentsManager = options.contentsManager;
171
200
  this._docManager = options.docManager;
172
201
  this._onOpenFile = options.onOpenFile;
202
+ this._translator = options.translator;
173
203
  this.id = FILE_BROWSER_ID;
174
204
  this.title.icon = xtralabFileBrowserIcon;
175
205
  this.title.caption = 'xtralab File Browser';
176
- this.addClass(FILE_BROWSER_CSS_CLASS);
206
+ this.addClass(PANEL_CSS_CLASS);
177
207
 
178
208
  this._toolbar = new Toolbar();
179
209
  this._toolbar.addClass(TOOLBAR_CSS_CLASS);
180
210
  this._toolbar.node.setAttribute('aria-label', 'file browser');
181
- layout.addWidget(this._toolbar);
182
211
 
183
212
  this._content = new XtralabFileTreeContent({
184
213
  contentsManager: this._contentsManager,
185
214
  docManager: this._docManager,
186
215
  onOpenFile: this._onOpenFile,
216
+ translator: this._translator,
187
217
  browser: this
188
218
  });
189
219
  this._content.addClass(CONTENT_CSS_CLASS);
190
- layout.addWidget(this._content);
220
+
221
+ this._section = new PanelWithToolbar({ toolbar: this._toolbar });
222
+ this._section.id = TREE_SECTION_ID;
223
+ // Only an accordion host draws this; a plain `PanelLayout` shows no title.
224
+ this._section.title.label = 'Files';
225
+ this._section.addClass(FILE_BROWSER_CSS_CLASS);
226
+ this._section.addWidget(this._toolbar);
227
+ this._section.addWidget(this._content);
228
+ layout.addWidget(this._section);
191
229
  }
192
230
 
193
231
  get contentsManager(): Contents.IManager {
@@ -280,9 +318,145 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
280
318
  this.setFileFilterVisible(!this._fileFilterVisible);
281
319
  }
282
320
 
321
+ get sectionNode(): HTMLElement {
322
+ return this._section.node;
323
+ }
324
+
325
+ get isEmpty(): boolean {
326
+ return this._panelLayout.widgets.length === 0;
327
+ }
328
+
329
+ get contentChanged(): ISignal<this, void> {
330
+ return this._contentChanged;
331
+ }
332
+
333
+ get sectionAdded(): ISignal<this, ISectionEntry> {
334
+ return this._sectionAdded;
335
+ }
336
+
337
+ get accordionPanel(): AccordionPanel | null {
338
+ return this._accordion;
339
+ }
340
+
341
+ get sections(): ReadonlyArray<Widget> {
342
+ if (!this._accordion) {
343
+ return [];
344
+ }
345
+ return this._accordion.widgets.filter(w => w !== this._section);
346
+ }
347
+
348
+ /**
349
+ * The toolbar is the section's title node: it is the tree's header row at
350
+ * home, and an accordion host renders it inside the section title.
351
+ */
352
+ getSections(): ReadonlyArray<ISectionEntry> {
353
+ return this._isSectionHome()
354
+ ? [
355
+ {
356
+ id: TREE_SECTION_ID,
357
+ titleNode: this._toolbar.node,
358
+ widget: this._section
359
+ }
360
+ ]
361
+ : [];
362
+ }
363
+
364
+ removeSectionById(sectionId: string): Widget | null {
365
+ if (sectionId !== TREE_SECTION_ID || !this._isSectionHome()) {
366
+ return null;
367
+ }
368
+ this._section.parent = null;
369
+ if (this._accordion && this._accordion.widgets.length === 0) {
370
+ this._accordion.dispose();
371
+ this._accordion = null;
372
+ }
373
+ // Away from home the toolbar must not offer "Move to …" as well;
374
+ // `getSections` re-marks it on the way back.
375
+ this._toolbar.node.classList.remove(MOVABLE_SECTION_CLASS);
376
+ this._contentChanged.emit();
377
+ return this._section;
378
+ }
379
+
380
+ reinsertSection(widget: Widget): void {
381
+ if (this._accordion) {
382
+ this._accordion.insertWidget(0, widget);
383
+ } else {
384
+ this._panelLayout.addWidget(widget);
385
+ this._restoreToolbar();
386
+ }
387
+ this._contentChanged.emit();
388
+ }
389
+
390
+ /**
391
+ * The move plugin reconciles persisted moves only against sections announced
392
+ * after `registerSource`, and this panel builds its section in the constructor.
393
+ */
394
+ announceSections(): void {
395
+ for (const entry of this.getSections()) {
396
+ this._sectionAdded.emit(entry);
397
+ }
398
+ }
399
+
400
+ /**
401
+ * The first hosted section moves the tree into an accordion.
402
+ */
403
+ addSection(widget: Widget): void {
404
+ if (!this._accordion) {
405
+ const accordion = new AccordionPanel({
406
+ layout: AccordionToolbar.createLayout({})
407
+ });
408
+ accordion.addClass(ACCORDION_CSS_CLASS);
409
+ if (this._section.parent === this) {
410
+ accordion.addWidget(this._section);
411
+ }
412
+ accordion.addWidget(widget);
413
+ this._accordion = accordion;
414
+ this._panelLayout.addWidget(accordion);
415
+ } else {
416
+ this._accordion.addWidget(widget);
417
+ }
418
+ this._contentChanged.emit();
419
+ }
420
+
421
+ removeSectionWidget(widget: Widget): void {
422
+ if (!this._accordion || widget.parent !== this._accordion) {
423
+ return;
424
+ }
425
+ widget.parent = null;
426
+ const remaining = this._accordion.widgets;
427
+ const treeOnly = remaining.length === 1 && remaining[0] === this._section;
428
+ if (treeOnly || remaining.length === 0) {
429
+ if (treeOnly) {
430
+ if (this._section.isHidden) {
431
+ this._section.show();
432
+ }
433
+ this._panelLayout.addWidget(this._section);
434
+ this._restoreToolbar();
435
+ }
436
+ this._accordion.dispose();
437
+ this._accordion = null;
438
+ }
439
+ this._contentChanged.emit();
440
+ }
441
+
442
+ private _isSectionHome(): boolean {
443
+ const parent = this._section.parent;
444
+ return parent === this || (!!this._accordion && parent === this._accordion);
445
+ }
446
+
447
+ /**
448
+ * An accordion host moves the toolbar node into the section title and leaves
449
+ * it orphaned on detach; re-parenting makes the layout attach it again.
450
+ */
451
+ private _restoreToolbar(): void {
452
+ this._toolbar.parent = null;
453
+ this._section.insertWidget(0, this._toolbar);
454
+ }
455
+
283
456
  private _contentsManager: Contents.IManager;
284
457
  private _docManager: IDocumentManager;
285
458
  private _onOpenFile: ((serverPath: string) => void) | undefined;
459
+ private _translator: ITranslator | undefined;
286
460
  private _selectedPaths: readonly string[] = [];
287
461
  private _selectionChanged = new Signal<this, readonly string[]>(this);
288
462
  private _refreshRequested = new Signal<this, void>(this);
@@ -294,12 +468,18 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
294
468
  private _fileFilterVisibleChanged = new Signal<this, boolean>(this);
295
469
  private _toolbar: Toolbar;
296
470
  private _content: XtralabFileTreeContent;
471
+ private _section: PanelWithToolbar;
472
+ private _panelLayout: PanelLayout;
473
+ private _accordion: AccordionPanel | null = null;
474
+ private _contentChanged = new Signal<this, void>(this);
475
+ private _sectionAdded = new Signal<this, ISectionEntry>(this);
297
476
  }
298
477
 
299
478
  interface IFileTreeContentOptions {
300
479
  contentsManager: Contents.IManager;
301
480
  docManager: IDocumentManager;
302
481
  onOpenFile?: (serverPath: string) => void;
482
+ translator?: ITranslator;
303
483
  browser: XtralabFileBrowser;
304
484
  }
305
485
 
@@ -319,6 +499,7 @@ class XtralabFileTreeContent extends ReactWidget {
319
499
  contentsManager={this._options.contentsManager}
320
500
  docManager={this._options.docManager}
321
501
  onOpenFile={this._options.onOpenFile}
502
+ translator={this._options.translator}
322
503
  widget={this._options.browser}
323
504
  />
324
505
  );
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import aboutPlugin from './about';
4
4
  import agentSessionsPlugin from './agentSessions';
5
5
  import askAgentPlugin from './askAgent';
6
6
  import commandBarPlugin from './commandBar';
7
+ import customPanelPlugin from './customPanel';
7
8
  import fileBrowserPlugin from './fileBrowser';
8
9
  import fileTypeIconsPlugin from './fileTypeIcons';
9
10
  import editorBreadcrumbsPlugin from './editorBreadcrumbs';
@@ -11,6 +12,7 @@ import editorIndentPlugin from './editorIndent';
11
12
  import gitPlugins from './git';
12
13
  import highlightPlugin from './highlight';
13
14
  import launcherPlugins from './launcher';
15
+ import menuBarPlugin from './menuBar';
14
16
  import menusPlugin from './menus';
15
17
  import omniboxPlugin from './omnibox';
16
18
  import searchReplacePlugin from './searchReplace';
@@ -35,6 +37,7 @@ const plugins: JupyterFrontEndPlugin<unknown>[] = [
35
37
  agentSessionsPlugin,
36
38
  askAgentPlugin,
37
39
  commandBarPlugin,
40
+ customPanelPlugin,
38
41
  editorBreadcrumbsPlugin,
39
42
  editorIndentPlugin,
40
43
  fileBrowserPlugin,
@@ -42,6 +45,7 @@ const plugins: JupyterFrontEndPlugin<unknown>[] = [
42
45
  ...gitPlugins,
43
46
  highlightPlugin,
44
47
  ...launcherPlugins,
48
+ menuBarPlugin,
45
49
  menusPlugin,
46
50
  omniboxPlugin,
47
51
  searchReplacePlugin,
@@ -26,7 +26,7 @@ export interface IAgent {
26
26
  * the user types one into the launcher's prompt box. The semantics:
27
27
  *
28
28
  * - `[]` → prompt is appended as a positional argument:
29
- * `<command> 'PROMPT'`. (Used by claude, codex, vibe.)
29
+ * `<command> 'PROMPT'`. (Used by claude, codex, vibe, pi.)
30
30
  * - `['-i']` / `['--prompt']` → the prompt is preceded by a flag:
31
31
  * `<command> -i 'PROMPT'`. (Used by copilot, opencode.)
32
32
  * - `undefined` → the agent does not accept an initial prompt. The
@@ -86,8 +86,9 @@ export interface IAgentSettings {
86
86
  * icons come from), but use the bare CLI names a user would type in a
87
87
  * terminal rather than the ACP wrapper binaries that project spawns.
88
88
  * Antigravity (Google's agent-first IDE/CLI, `agy`) is the Google agent.
89
- * Anything not on the user's `$PATH` is filtered out at activation time, so
90
- * the wider list is harmless.
89
+ * Pi (https://pi.dev, `earendil-works/pi`) is a minimal extensible coding
90
+ * agent; its mark comes straight from its site. Anything not on the user's
91
+ * `$PATH` is filtered out at activation time, so the wider list is harmless.
91
92
  */
92
93
  const DEFAULTS: IAgent[] = [
93
94
  {
@@ -179,6 +180,18 @@ const DEFAULTS: IAgent[] = [
179
180
  rank: 7,
180
181
  requireAvailable: true,
181
182
  promptArgs: []
183
+ },
184
+ {
185
+ id: 'pi',
186
+ label: 'Pi',
187
+ caption: 'Start Pi in a new terminal.',
188
+ command: 'pi',
189
+ icon: BUILTIN_AGENT_ICONS.pi,
190
+ rank: 8,
191
+ requireAvailable: true,
192
+ // `pi [messages...]` starts the interactive TUI and sends the messages
193
+ // as the first user prompt, so the positional form applies.
194
+ promptArgs: []
182
195
  }
183
196
  ];
184
197
 
@@ -6,11 +6,14 @@ import { LabIcon } from '@jupyterlab/ui-components';
6
6
  * (`jupyter_ai_acp_client/static/*.svg`, BSD-3-Clause); Antigravity post-dates
7
7
  * that set, so its mark is the official glyph from `@lobehub/icons-static-svg`
8
8
  * (MIT) — we ship the brand-colored `antigravity-color` variant so Antigravity
9
- * reads as a multi-color Google mark. Small adjustments are applied
9
+ * reads as a multi-color Google mark. Pi's mark is the official `logo-auto.svg`
10
+ * from https://pi.dev (MIT, `earendil-works/pi`). Small adjustments are applied
10
11
  * to play nicely with JupyterLab themes:
11
- * - Monochrome marks (Codex, Copilot, Goose) use `currentColor` so they
12
+ * - Monochrome marks (Codex, Copilot, Goose, Pi) use `currentColor` so they
12
13
  * inherit the surrounding launcher card's text color rather than
13
- * hard-coding black, which disappears on the dark theme.
14
+ * hard-coding black, which disappears on the dark theme. For Pi this
15
+ * replaces the upstream `prefers-color-scheme` style block, which follows
16
+ * the OS scheme rather than the active JupyterLab theme.
14
17
  * - The Goose mark upstream is rendered on a white rounded rect; we drop
15
18
  * the rect so the silhouette can sit on the launcher card directly.
16
19
  * - SVG ids that would collide if two copies of the same artwork ended up
@@ -179,6 +182,14 @@ const opencodeIcon = new LabIcon({
179
182
  </svg>`
180
183
  });
181
184
 
185
+ const piIcon = new LabIcon({
186
+ name: 'xtralab:agent-pi',
187
+ svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" fill="currentColor">
188
+ <path fill-rule="evenodd" d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"/>
189
+ <path d="M517.36 400H634.72V634.72H517.36Z"/>
190
+ </svg>`
191
+ });
192
+
182
193
  const neovimIcon = new LabIcon({
183
194
  name: 'xtralab:editor-neovim',
184
195
  svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#57A143">
@@ -207,7 +218,8 @@ export const BUILTIN_AGENT_ICONS: Record<string, LabIcon> = {
207
218
  goose: gooseIcon,
208
219
  kiro: kiroIcon,
209
220
  'mistral-vibe': mistralVibeIcon,
210
- opencode: opencodeIcon
221
+ opencode: opencodeIcon,
222
+ pi: piIcon
211
223
  };
212
224
 
213
225
  /**
@@ -10,21 +10,39 @@ import { defaultEditorSettings } from './editors';
10
10
  export const LAUNCHER_PLUGIN_ID = 'xtralab:launcher';
11
11
 
12
12
  /**
13
- * Setting registries that already have the transform, so both callers skip
14
- * re-registering (the registry rejects a duplicate transformer).
13
+ * Registries that already have the transform (duplicates are rejected).
15
14
  */
16
15
  const registered = new WeakSet<ISettingRegistry>();
17
16
 
18
17
  /**
19
- * Register a `fetch` transform that injects xtralab's built-in agent and
20
- * editor lists as the schema defaults for the `xtralab:launcher` settings, so
21
- * the Settings Editor shows the shipped list.
22
- *
23
- * The schema declares `jupyter.lab.transform: true`, so the registry defers
24
- * loading the plugin until a transform is registered: callers must invoke this
25
- * before their first `settingRegistry.load(LAUNCHER_PLUGIN_ID)`. Idempotent, so
26
- * the two plugins that load these settings can both call it regardless of
27
- * activation order.
18
+ * Merge sparse user entries over the built-in defaults, keyed by `id`.
19
+ */
20
+ function mergeSettingsById<
21
+ T extends { id: string; enabled?: boolean; requireAvailable?: boolean }
22
+ >(defaults: T[], user: PartialJSONValue | undefined): T[] {
23
+ const entries = Array.isArray(user) ? (user as unknown as T[]) : [];
24
+ const overrideById = new Map(entries.map(entry => [entry.id, entry]));
25
+ const merged = defaults.map(base => {
26
+ const override = overrideById.get(base.id);
27
+ if (!override) {
28
+ return base;
29
+ }
30
+ overrideById.delete(base.id);
31
+ return { ...base, ...override };
32
+ });
33
+ merged.push(...overrideById.values());
34
+ // Item schema defaults are applied before this transform runs; without the
35
+ // refill, sparse entries render with unchecked boxes.
36
+ return merged.map(entry => ({
37
+ ...entry,
38
+ enabled: entry.enabled ?? true,
39
+ requireAvailable: entry.requireAvailable ?? true
40
+ }));
41
+ }
42
+
43
+ /**
44
+ * Must run before the first `load(LAUNCHER_PLUGIN_ID)` (the schema sets
45
+ * `jupyter.lab.transform: true`); idempotent across callers.
28
46
  */
29
47
  export function registerLauncherSchemaDefaults(
30
48
  settingRegistry: ISettingRegistry
@@ -35,11 +53,23 @@ export function registerLauncherSchemaDefaults(
35
53
  registered.add(settingRegistry);
36
54
 
37
55
  settingRegistry.transform(LAUNCHER_PLUGIN_ID, {
56
+ compose: plugin => {
57
+ const { user } = plugin.data;
58
+ const composite = { ...plugin.data.composite };
59
+ composite.agents = mergeSettingsById(
60
+ defaultAgentSettings(),
61
+ user.agents
62
+ ) as unknown as PartialJSONValue;
63
+ composite.editors = mergeSettingsById(
64
+ defaultEditorSettings(),
65
+ user.editors
66
+ ) as unknown as PartialJSONValue;
67
+ plugin.data = { composite, user };
68
+ return plugin;
69
+ },
38
70
  fetch: plugin => {
39
71
  const properties = plugin.schema.properties;
40
72
  if (properties) {
41
- // The settings shapes are plain JSON by construction, but their named
42
- // interfaces lack an index signature, so cast at this TS→JSON boundary.
43
73
  if (properties.agents) {
44
74
  properties.agents.default =
45
75
  defaultAgentSettings() as unknown as PartialJSONValue;
@@ -0,0 +1,14 @@
1
+ import { LabIcon } from '@jupyterlab/ui-components';
2
+
3
+ /**
4
+ * Hamburger mark for the collapsed main-menu button, matching the sidebar
5
+ * toggles in ../topBar/icons.ts.
6
+ */
7
+ export const mainMenuIcon = new LabIcon({
8
+ name: 'xtralab:main-menu',
9
+ svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
10
+ <line x1="4.5" y1="7" x2="19.5" y2="7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
11
+ <line x1="4.5" y1="12" x2="19.5" y2="12" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
12
+ <line x1="4.5" y1="17" x2="19.5" y2="17" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
13
+ </svg>`
14
+ });