xtralab 0.8.0 → 0.10.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 (57) hide show
  1. package/README.md +30 -6
  2. package/lib/fileBrowser/commands.d.ts +7 -0
  3. package/lib/fileBrowser/commands.js +74 -1
  4. package/lib/fileBrowser/fileBrowser.js +4 -1
  5. package/lib/fileBrowser/icons.d.ts +5 -9
  6. package/lib/fileBrowser/icons.js +97 -13
  7. package/lib/fileBrowser/index.js +5 -3
  8. package/lib/fileTypeIcons/index.d.ts +6 -0
  9. package/lib/fileTypeIcons/index.js +144 -0
  10. package/lib/git/commands.d.ts +34 -33
  11. package/lib/git/commands.js +132 -10
  12. package/lib/git/diffModel.d.ts +14 -0
  13. package/lib/git/diffModel.js +75 -0
  14. package/lib/git/diffProvider.d.ts +0 -5
  15. package/lib/git/diffProvider.js +6 -376
  16. package/lib/git/diffSurface.d.ts +36 -26
  17. package/lib/git/diffSurface.js +13 -20
  18. package/lib/git/diffTheme.d.ts +9 -0
  19. package/lib/git/diffTheme.js +31 -0
  20. package/lib/git/diffWidget.d.ts +54 -91
  21. package/lib/git/diffWidget.js +184 -361
  22. package/lib/git/index.d.ts +1 -6
  23. package/lib/git/index.js +7 -23
  24. package/lib/git/notebookDiff.d.ts +6 -1
  25. package/lib/git/notebookDiff.js +4 -3
  26. package/lib/index.js +4 -0
  27. package/lib/terminalNotifications/index.d.ts +15 -0
  28. package/lib/terminalNotifications/index.js +213 -0
  29. package/lib/terminals/model.d.ts +1 -1
  30. package/lib/terminals/model.js +1 -1
  31. package/lib/terminals/widget.d.ts +1 -1
  32. package/lib/terminals/widget.js +1 -1
  33. package/package.json +3 -3
  34. package/schema/plugin.json +6 -0
  35. package/schema/terminal-notifications.json +21 -0
  36. package/src/fileBrowser/commands.ts +91 -2
  37. package/src/fileBrowser/fileBrowser.tsx +4 -1
  38. package/src/fileBrowser/icons.ts +103 -13
  39. package/src/fileBrowser/index.ts +9 -3
  40. package/src/fileTypeIcons/index.ts +174 -0
  41. package/src/git/commands.ts +177 -43
  42. package/src/git/diffModel.ts +102 -0
  43. package/src/git/diffProvider.tsx +8 -540
  44. package/src/git/diffSurface.tsx +44 -28
  45. package/src/git/diffTheme.ts +40 -0
  46. package/src/git/diffWidget.tsx +256 -448
  47. package/src/git/index.ts +14 -28
  48. package/src/git/notebookDiff.tsx +10 -3
  49. package/src/index.ts +4 -0
  50. package/src/terminalNotifications/index.ts +295 -0
  51. package/src/terminals/model.ts +1 -1
  52. package/src/terminals/widget.tsx +1 -1
  53. package/style/git.css +12 -9
  54. package/style/index.css +1 -0
  55. package/style/index.js +1 -0
  56. package/style/launcher.css +46 -18
  57. package/style/sidebar.css +14 -0
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  An opinionated JupyterLab meta-package for use with coding agents.
4
4
 
5
5
  It bundles a curated set of JupyterLab extensions, a path-first file browser,
6
- git diffs rendered by xtralab, an agent launcher, and a quieter default
7
- workspace.
6
+ rich git diffs for text, notebooks, and images, an agent launcher, and a
7
+ quieter default workspace.
8
8
 
9
9
  ![xtralab screenshot](./screenshot.png)
10
10
 
@@ -34,6 +34,23 @@ from `main` are uploaded as workflow artifacts on the
34
34
  [CONTRIBUTING.md](./CONTRIBUTING.md) for the desktop app architecture and local
35
35
  build instructions.
36
36
 
37
+ On macOS the build is not notarized yet (it has no Apple Developer ID), so
38
+ Gatekeeper blocks it on first launch, and agent notifications fall back to
39
+ `osascript`, appearing as "Script Editor". In the meantime you can self-sign it
40
+ for free: this lets it launch and gives native notifications instead, branded as
41
+ xtralab and clickable to jump to the terminal that fired them. Create a
42
+ code-signing certificate once in Keychain Access (Certificate Assistant, Create
43
+ a Certificate, with Identity Type "Self Signed Root" and Certificate Type "Code
44
+ Signing") named `xtralab-selfsign`, then sign the app you copied out of the DMG:
45
+
46
+ ```bash
47
+ codesign --force --deep --sign "xtralab-selfsign" --timestamp=none "/Applications/xtralab.app"
48
+ xattr -dr com.apple.quarantine "/Applications/xtralab.app"
49
+ ```
50
+
51
+ macOS prompts once to let `codesign` use the key; choose Always Allow. None of
52
+ this will be needed once the app ships with a Developer ID signature.
53
+
37
54
  ## What's included
38
55
 
39
56
  xtralab builds on [`ajlab`](https://github.com/jtpio/ajlab), the agent-ready
@@ -46,10 +63,11 @@ themes (`jupyterlab-cursor-light`, `jupyterlab-cursor-dark`,
46
63
 
47
64
  The bundled xtralab labextension then adds:
48
65
 
49
- - A path-first file browser in the left sidebar.
50
- - Git diffs rendered by xtralab in the `jupyterlab-git` panel, using
51
- `@pierre/diffs` for text and notebooks and a 2-up / swipe / onion-skin view
52
- for images.
66
+ - A path-first file browser in the left sidebar, with `@pierre/trees`
67
+ file-type icons that also carry over to document tabs and the `jupyterlab-git`
68
+ panel.
69
+ - Rich git diffs in the `jupyterlab-git` panel, using `@pierre/diffs` for text
70
+ and notebooks and a 2-up / swipe / onion-skin view for images.
53
71
  - An agent launcher with a prompt box, buttons for the agents installed on your
54
72
  machine (Claude, Codex, Antigravity, Copilot, Goose, OpenCode, Kiro, Mistral
55
73
  Vibe), a collapsible list of changed files, and an Open row for a terminal,
@@ -59,6 +77,12 @@ The bundled xtralab labextension then adds:
59
77
  the same icon.
60
78
  - Sidebar toggle buttons in the top bar for the left and right areas.
61
79
 
80
+ xtralab also ships a set of opinionated defaults: unused UI is hidden for a
81
+ quieter workspace, the activity bar sits at the top, and autocompletion,
82
+ continuous LSP hinting, code folding, and gitignore-aware quick open are on by
83
+ default. See [the bundled labconfig overrides](./jupyter-config/labconfig) for
84
+ the full set.
85
+
62
86
  ## Connecting agents to Jupyter (MCP)
63
87
 
64
88
  xtralab runs a [Model Context Protocol][mcp-spec] server inside JupyterLab,
@@ -1,4 +1,7 @@
1
1
  import { JupyterFrontEnd } from '@jupyterlab/application';
2
+ import { ICommandPalette } from '@jupyterlab/apputils';
3
+ import { IDocumentManager } from '@jupyterlab/docmanager';
4
+ import { ITranslator } from '@jupyterlab/translation';
2
5
  import { IXtralabFileBrowser } from './widget';
3
6
  /**
4
7
  * Command identifiers exposed by the xtralab browser. We deliberately namespace
@@ -20,6 +23,7 @@ export declare namespace CommandIDs {
20
23
  const createNewDirectory = "xtralab:create-new-directory";
21
24
  const newLauncher = "xtralab:new-launcher";
22
25
  const revealPath = "xtralab:reveal-path";
26
+ const revealInFileTree = "xtralab:reveal-in-file-tree";
23
27
  }
24
28
  /**
25
29
  * Submenu id used to host the dynamically populated "Open With" entries.
@@ -31,6 +35,9 @@ export declare const OPEN_WITH_SUBMENU_ID = "jp-contextmenu-xtralab-open-with";
31
35
  export interface IRegisterCommandsOptions {
32
36
  app: JupyterFrontEnd;
33
37
  browser: IXtralabFileBrowser;
38
+ docManager: IDocumentManager;
39
+ palette: ICommandPalette | null;
40
+ translator: ITranslator | null;
34
41
  }
35
42
  /**
36
43
  * Register every xtralab command on the application command registry, attach
@@ -1,5 +1,6 @@
1
1
  import { Clipboard, Dialog, InputDialog, showDialog, showErrorMessage } from '@jupyterlab/apputils';
2
2
  import { PageConfig, PathExt } from '@jupyterlab/coreutils';
3
+ import { nullTranslator } from '@jupyterlab/translation';
3
4
  import { addIcon, closeIcon, collapseAllIcon, CommandToolbarButton, copyIcon, downloadIcon, editIcon, fileIcon, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
4
5
  import { toCanonicalPath, toServerPath } from './contents';
5
6
  import { FILE_BROWSER_ID } from './widget';
@@ -24,6 +25,7 @@ export var CommandIDs;
24
25
  CommandIDs.createNewDirectory = 'xtralab:create-new-directory';
25
26
  CommandIDs.newLauncher = 'xtralab:new-launcher';
26
27
  CommandIDs.revealPath = 'xtralab:reveal-path';
28
+ CommandIDs.revealInFileTree = 'xtralab:reveal-in-file-tree';
27
29
  })(CommandIDs || (CommandIDs = {}));
28
30
  /**
29
31
  * Submenu id used to host the dynamically populated "Open With" entries.
@@ -102,6 +104,44 @@ function getWorkingDirectory(browser) {
102
104
  }
103
105
  return PathExt.dirname(serverPath);
104
106
  }
107
+ /**
108
+ * Resolve the main-area widget whose tab is the target of the current
109
+ * context-menu event, or `null` when that target is not a document tab. The
110
+ * shell stamps each tab's `data-id` with its widget id, so the right-clicked
111
+ * tab resolves even when it is not the current one.
112
+ *
113
+ * Valid only during a context-menu invocation: `contextMenuHitTest` reads the
114
+ * last context-menu event, which is not cleared when the menu closes, so
115
+ * {@link documentPathToReveal} gates this to avoid resolving a stale tab.
116
+ */
117
+ function contextMenuTabWidget(app) {
118
+ const node = app.contextMenuHitTest(n => n.dataset !== undefined && n.dataset.type === 'document-title');
119
+ const id = node === null || node === void 0 ? void 0 : node.dataset.id;
120
+ if (id === undefined) {
121
+ return null;
122
+ }
123
+ for (const widget of app.shell.widgets('main')) {
124
+ if (widget.id === id) {
125
+ return widget;
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+ /**
131
+ * Resolve the document to reveal as a canonical `@pierre/trees` path: the
132
+ * right-clicked tab when `fromContextMenu` is true (the file-tab menu),
133
+ * otherwise the active main-area widget (the palette). A file's context
134
+ * `path` is already canonical. Returns `undefined` when nothing resolves.
135
+ */
136
+ function documentPathToReveal(app, docManager, fromContextMenu) {
137
+ var _a, _b, _c;
138
+ const widget = (_a = (fromContextMenu ? contextMenuTabWidget(app) : null)) !== null && _a !== void 0 ? _a : app.shell.currentWidget;
139
+ if (widget === null) {
140
+ return undefined;
141
+ }
142
+ const path = (_c = (_b = docManager.contextForWidget(widget)) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : '';
143
+ return path.length > 0 ? path : undefined;
144
+ }
105
145
  /**
106
146
  * Refresh the "Open With" submenu's items from the document factories that
107
147
  * can open the file under the right-click. Mirrors the
@@ -148,8 +188,10 @@ function makeOpenWithUpdater(app, browser) {
148
188
  * the plugin.
149
189
  */
150
190
  export function registerCommands(opts) {
151
- const { app, browser } = opts;
191
+ var _a;
192
+ const { app, browser, docManager, palette, translator } = opts;
152
193
  const { commands } = app;
194
+ const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
153
195
  commands.addCommand(CommandIDs.open, {
154
196
  label: args => { var _a, _b; return ((_b = (_a = args.label) !== null && _a !== void 0 ? _a : args.factory) !== null && _b !== void 0 ? _b : 'Open'); },
155
197
  icon: args => {
@@ -364,6 +406,21 @@ export function registerCommands(opts) {
364
406
  }
365
407
  }
366
408
  });
409
+ // Reveal the resolved document in the tree browser via the reveal-path
410
+ // command, which surfaces the sidebar and scrolls to the target.
411
+ commands.addCommand(CommandIDs.revealInFileTree, {
412
+ label: trans.__('Show in File Tree'),
413
+ caption: trans.__('Show this file in the xtralab file browser'),
414
+ isEnabled: args => documentPathToReveal(app, docManager, args.fromTab === true) !==
415
+ undefined,
416
+ execute: args => {
417
+ const path = documentPathToReveal(app, docManager, args.fromTab === true);
418
+ if (path === undefined) {
419
+ return undefined;
420
+ }
421
+ return commands.execute(CommandIDs.revealPath, { path });
422
+ }
423
+ });
367
424
  commands.addCommand(CommandIDs.createNewDirectory, {
368
425
  label: 'New Folder',
369
426
  caption: 'Create a new folder',
@@ -402,6 +459,19 @@ export function registerCommands(opts) {
402
459
  commands.notifyCommandChanged(id);
403
460
  }
404
461
  });
462
+ // This command's enabled state tracks the active widget, not the tree
463
+ // selection, so re-evaluate it on current-widget changes. `currentChanged`
464
+ // is optional on the shell interface.
465
+ const onCurrentChanged = () => {
466
+ commands.notifyCommandChanged(CommandIDs.revealInFileTree);
467
+ };
468
+ (_a = app.shell.currentChanged) === null || _a === void 0 ? void 0 : _a.connect(onCurrentChanged);
469
+ // Also surface it in the palette (acts on the active editor); the file-tab
470
+ // context-menu entry is declared in `schema/plugin.json`.
471
+ const paletteItem = palette === null || palette === void 0 ? void 0 : palette.addItem({
472
+ command: CommandIDs.revealInFileTree,
473
+ category: trans.__('File Browser')
474
+ });
405
475
  // The static items are declared in `schema/plugin.json` under
406
476
  // `jupyter.lab.menus.context` so users can override or disable them. The
407
477
  // schema also declares an empty submenu placeholder with id
@@ -410,7 +480,10 @@ export function registerCommands(opts) {
410
480
  const updateOpenWithMenu = makeOpenWithUpdater(app, browser);
411
481
  app.contextMenu.opened.connect(updateOpenWithMenu);
412
482
  return () => {
483
+ var _a;
413
484
  app.contextMenu.opened.disconnect(updateOpenWithMenu);
485
+ (_a = app.shell.currentChanged) === null || _a === void 0 ? void 0 : _a.disconnect(onCurrentChanged);
486
+ paletteItem === null || paletteItem === void 0 ? void 0 : paletteItem.dispose();
414
487
  };
415
488
  }
416
489
  /**
@@ -562,7 +562,10 @@ export function FileBrowserComponent(props) {
562
562
  previous === null || previous === void 0 ? void 0 : previous.deselect();
563
563
  }
564
564
  target.select();
565
- model.focusPath(canonicalPath);
565
+ // `select` highlights the row; `scrollToPath` focuses it and scrolls it
566
+ // to the middle of the viewport, even when the row is virtualized out of
567
+ // the rendered window.
568
+ model.scrollToPath(canonicalPath, { focus: true, offset: 'center' });
566
569
  };
567
570
  /**
568
571
  * Insert a newly-created path (typically from "new folder" or
@@ -24,14 +24,10 @@ export declare const xtralabFileBrowserIcon: LabIcon;
24
24
  */
25
25
  export declare const FILE_BROWSER_ICONS: FileTreeIconConfig;
26
26
  /**
27
- * Return a JupyterLab `LabIcon` matching the icon the xtralab file tree would
28
- * render for the given file path. Used by other plugins (e.g. the git diff
29
- * viewer) that want their main-area widget tabs to display the same per-file
30
- * glyph the tree shows in the sidebar.
31
- *
32
- * Resolves the symbol via the same `@pierre/trees` resolver the tree itself
33
- * uses, then inlines the symbol's body into a self-contained SVG so the icon
34
- * works outside the tree's shadow DOM. Falls back to JupyterLab's default
35
- * `fileIcon` when the sprite cannot be located in either sheet.
27
+ * Return the icon the xtralab file tree would render for `filePath`.
36
28
  */
37
29
  export declare function getTreeIcon(filePath: string): LabIcon;
30
+ /**
31
+ * Like {@link getTreeIcon}, but returns `null` for the generic file glyph.
32
+ */
33
+ export declare function getSpecificTreeIcon(filePath: string): LabIcon | null;
@@ -1,6 +1,6 @@
1
1
  var _a;
2
2
  import { LabIcon, fileIcon } from '@jupyterlab/ui-components';
3
- import { createFileTreeIconResolver, getBuiltInFileIconColor, getBuiltInSpriteSheet } from '@pierre/trees';
3
+ import { createFileTreeIconResolver, getBuiltInSpriteSheet } from '@pierre/trees';
4
4
  /**
5
5
  * The glyph used as the xtralab file browser's sidebar tab icon. Defined as a
6
6
  * `LabIcon` (rather than the default `folderIcon` from
@@ -105,26 +105,96 @@ function extractSymbol(symbolId) {
105
105
  };
106
106
  }
107
107
  /**
108
- * Return a JupyterLab `LabIcon` matching the icon the xtralab file tree would
109
- * render for the given file path. Used by other plugins (e.g. the git diff
110
- * viewer) that want their main-area widget tabs to display the same per-file
111
- * glyph the tree shows in the sidebar.
108
+ * Fallback colors for the built-in file icons, keyed by the icon token
109
+ * `@pierre/trees` resolves for a path (e.g. `python`, `typescript`).
112
110
  *
113
- * Resolves the symbol via the same `@pierre/trees` resolver the tree itself
114
- * uses, then inlines the symbol's body into a self-contained SVG so the icon
115
- * works outside the tree's shadow DOM. Falls back to JupyterLab's default
116
- * `fileIcon` when the sprite cannot be located in either sheet.
111
+ * The tree only exposes these as `--trees-file-icon-color-<token>` CSS
112
+ * variables scoped to its shadow host, so they resolve to nothing for the
113
+ * standalone {@link LabIcon}s {@link getTreeIcon} builds for main-area tabs.
114
+ * Mirroring them here lets {@link getBuiltInFileIconColor} bottom out in a
115
+ * real color. Each is a `light-dark(…)` pair for dark themes.
117
116
  */
118
- export function getTreeIcon(filePath) {
117
+ const BUILT_IN_FILE_ICON_COLOR_FALLBACKS = {
118
+ astro: 'light-dark(#a631be, #d568ea)',
119
+ babel: 'light-dark(#d5a910, #ffd452)',
120
+ bash: 'light-dark(#199f43, #5ecc71)',
121
+ biome: 'light-dark(#1a85d4, #69b1ff)',
122
+ bootstrap: 'light-dark(#693acf, #9d6afb)',
123
+ browserslist: 'light-dark(#d5a910, #ffd452)',
124
+ bun: 'light-dark(#594c5b, #79697b)',
125
+ c: 'light-dark(#1a85d4, #69b1ff)',
126
+ claude: 'light-dark(#d47628, #ffa359)',
127
+ cpp: 'light-dark(#1a85d4, #69b1ff)',
128
+ css: 'light-dark(#693acf, #9d6afb)',
129
+ database: 'light-dark(#a631be, #d568ea)',
130
+ default: 'light-dark(#84848a, #adadb1)',
131
+ docker: 'light-dark(#1a85d4, #69b1ff)',
132
+ eslint: 'light-dark(#693acf, #9d6afb)',
133
+ git: 'light-dark(#ff8c5b, #d5512f)',
134
+ go: 'light-dark(#1ca1c7, #68cdf2)',
135
+ graphql: 'light-dark(#d32a61, #ff678d)',
136
+ html: 'light-dark(#d47628, #ffa359)',
137
+ image: 'light-dark(#d32a61, #ff678d)',
138
+ javascript: 'light-dark(#d5a910, #ffd452)',
139
+ json: 'light-dark(#d47628, #ffa359)',
140
+ markdown: 'light-dark(#199f43, #5ecc71)',
141
+ mcp: 'light-dark(#17a5af, #64d1db)',
142
+ npm: 'light-dark(#d52c36, #ff6762)',
143
+ oxc: 'light-dark(#1ca1c7, #68cdf2)',
144
+ postcss: 'light-dark(#d52c36, #ff6762)',
145
+ prettier: 'light-dark(#17a5af, #64d1db)',
146
+ python: 'light-dark(#1a85d4, #69b1ff)',
147
+ react: 'light-dark(#1ca1c7, #68cdf2)',
148
+ ruby: 'light-dark(#d52c36, #ff6762)',
149
+ rust: 'light-dark(#d47628, #ffa359)',
150
+ sass: 'light-dark(#d32a61, #ff678d)',
151
+ svelte: 'light-dark(#d52c36, #ff6762)',
152
+ svg: 'light-dark(#d47628, #ffa359)',
153
+ svgo: 'light-dark(#199f43, #5ecc71)',
154
+ swift: 'light-dark(#d47628, #ffa359)',
155
+ table: 'light-dark(#17a5af, #64d1db)',
156
+ tailwind: 'light-dark(#1ca1c7, #68cdf2)',
157
+ terraform: 'light-dark(#693acf, #9d6afb)',
158
+ text: 'light-dark(#84848a, #adadb1)',
159
+ typescript: 'light-dark(#1a85d4, #69b1ff)',
160
+ vite: 'light-dark(#a631be, #d568ea)',
161
+ vscode: 'light-dark(#1a85d4, #69b1ff)',
162
+ vue: 'light-dark(#199f43, #5ecc71)',
163
+ wasm: 'light-dark(#693acf, #9d6afb)',
164
+ webpack: 'light-dark(#1a85d4, #69b1ff)',
165
+ yml: 'light-dark(#d52c36, #ff6762)',
166
+ zig: 'light-dark(#d47628, #ffa359)',
167
+ zip: 'light-dark(#d47628, #ffa359)'
168
+ };
169
+ /**
170
+ * Color the file tree would paint for the built-in icon `token`, as a
171
+ * self-contained CSS value usable outside the tree's shadow DOM: the tree's
172
+ * `--trees-file-icon-color-*` properties when in scope, else the literal from
173
+ * {@link BUILT_IN_FILE_ICON_COLOR_FALLBACKS}. Returns `undefined` for unknown
174
+ * tokens.
175
+ */
176
+ function getBuiltInFileIconColor(token) {
177
+ const fallback = BUILT_IN_FILE_ICON_COLOR_FALLBACKS[token];
178
+ if (fallback === undefined) {
179
+ return undefined;
180
+ }
181
+ return `var(--trees-file-icon-color-${token}, var(--trees-file-icon-color, ${fallback}))`;
182
+ }
183
+ /**
184
+ * Resolve a tree glyph into a standalone `LabIcon`.
185
+ */
186
+ function resolveTreeIcon(filePath) {
119
187
  var _a;
120
188
  const resolved = treeIconResolver.resolveIcon('file-tree-icon-file', filePath);
189
+ // Anything other than the catch-all glyph is specific to this file.
190
+ const specific = resolved.token !== 'default' && resolved.name !== 'file-tree-icon-file';
121
191
  const cached = TREE_ICON_CACHE.get(resolved.name);
122
192
  if (cached !== undefined) {
123
- return cached;
193
+ return { icon: cached, specific };
124
194
  }
125
195
  const symbol = extractSymbol(resolved.name);
126
196
  if (symbol === null) {
127
- return fileIcon;
197
+ return { icon: null, specific };
128
198
  }
129
199
  // Built-in language icons paint with `fill="currentColor"` so the colored
130
200
  // tier of the tree can recolor them by setting `color` on the host
@@ -149,5 +219,19 @@ export function getTreeIcon(filePath) {
149
219
  svgstr
150
220
  });
151
221
  TREE_ICON_CACHE.set(resolved.name, icon);
152
- return icon;
222
+ return { icon, specific };
223
+ }
224
+ /**
225
+ * Return the icon the xtralab file tree would render for `filePath`.
226
+ */
227
+ export function getTreeIcon(filePath) {
228
+ var _a;
229
+ return (_a = resolveTreeIcon(filePath).icon) !== null && _a !== void 0 ? _a : fileIcon;
230
+ }
231
+ /**
232
+ * Like {@link getTreeIcon}, but returns `null` for the generic file glyph.
233
+ */
234
+ export function getSpecificTreeIcon(filePath) {
235
+ const { icon, specific } = resolveTreeIcon(filePath);
236
+ return specific ? icon : null;
153
237
  }
@@ -1,6 +1,8 @@
1
1
  import { ILayoutRestorer } from '@jupyterlab/application';
2
+ import { ICommandPalette } from '@jupyterlab/apputils';
2
3
  import { IDocumentManager } from '@jupyterlab/docmanager';
3
4
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
5
+ import { ITranslator } from '@jupyterlab/translation';
4
6
  import { populateToolbar, registerCommands } from './commands';
5
7
  import { XtralabFileBrowser } from './widget';
6
8
  const PLUGIN_ID = 'xtralab:plugin';
@@ -13,8 +15,8 @@ const plugin = {
13
15
  description: 'A path-first file browser for JupyterLab built on @pierre/trees.',
14
16
  autoStart: true,
15
17
  requires: [IDocumentManager],
16
- optional: [ILayoutRestorer, ISettingRegistry],
17
- activate: (app, docManager, restorer, settingRegistry) => {
18
+ optional: [ILayoutRestorer, ISettingRegistry, ICommandPalette, ITranslator],
19
+ activate: (app, docManager, restorer, settingRegistry, palette, translator) => {
18
20
  const browser = new XtralabFileBrowser({
19
21
  contentsManager: app.serviceManager.contents,
20
22
  docManager,
@@ -30,7 +32,7 @@ const plugin = {
30
32
  if (restorer) {
31
33
  restorer.add(browser, browser.id);
32
34
  }
33
- registerCommands({ app, browser });
35
+ registerCommands({ app, browser, docManager, palette, translator });
34
36
  populateToolbar({ app, browser });
35
37
  if (settingRegistry) {
36
38
  settingRegistry.load(PLUGIN_ID).catch(reason => {
@@ -0,0 +1,6 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ /**
3
+ * Share the file browser's tree glyphs with tabs and git file lists.
4
+ */
5
+ declare const plugin: JupyterFrontEndPlugin<void>;
6
+ export default plugin;
@@ -0,0 +1,144 @@
1
+ import { IDocumentWidgetOpener } from '@jupyterlab/docmanager';
2
+ import { IGitExtension } from '@jupyterlab/git';
3
+ import { getSpecificTreeIcon } from '../fileBrowser/icons';
4
+ const PLUGIN_ID = 'xtralab:file-type-icons';
5
+ /**
6
+ * Synthetic path used to resolve a file type's tree glyph.
7
+ */
8
+ function representativePath(fileType) {
9
+ var _a;
10
+ const extension = (_a = fileType.extensions) === null || _a === void 0 ? void 0 : _a[0];
11
+ if (extension === undefined || extension === '') {
12
+ return null;
13
+ }
14
+ // Extensions are stored with a leading dot (e.g. `.py`, `.tar.gz`).
15
+ return `file${extension.startsWith('.') ? extension : `.${extension}`}`;
16
+ }
17
+ /**
18
+ * Point a file type's icon at the tree glyph for its extension.
19
+ */
20
+ function applyTreeIcon(fileType) {
21
+ const path = representativePath(fileType);
22
+ if (path === null) {
23
+ return;
24
+ }
25
+ const icon = getSpecificTreeIcon(path);
26
+ if (icon === null) {
27
+ return;
28
+ }
29
+ // The registry owns these objects; `readonly` only blocks the type shape.
30
+ fileType.icon = icon;
31
+ }
32
+ /**
33
+ * Source extensions with tree glyphs but no default JupyterLab file type.
34
+ */
35
+ const EXTRA_FILE_TYPE_EXTENSIONS = [
36
+ '.ts',
37
+ '.tsx',
38
+ '.mts',
39
+ '.cts',
40
+ '.js',
41
+ '.jsx',
42
+ '.mjs',
43
+ '.cjs',
44
+ '.go',
45
+ '.rs',
46
+ '.rb',
47
+ '.c',
48
+ '.h',
49
+ '.cpp',
50
+ '.cc',
51
+ '.hpp',
52
+ '.swift',
53
+ '.zig',
54
+ '.vue',
55
+ '.svelte',
56
+ '.astro',
57
+ '.scss',
58
+ '.sass',
59
+ '.sh',
60
+ '.bash',
61
+ '.zsh',
62
+ '.graphql',
63
+ '.wasm'
64
+ ];
65
+ /**
66
+ * Register icon-only file types for unclaimed source extensions.
67
+ */
68
+ function registerExtraFileTypes(docRegistry) {
69
+ for (const extension of EXTRA_FILE_TYPE_EXTENSIONS) {
70
+ const icon = getSpecificTreeIcon(`file${extension}`);
71
+ if (icon === null) {
72
+ continue;
73
+ }
74
+ const claimed = docRegistry
75
+ .getFileTypesForPath(`file${extension}`)
76
+ .some(fileType => { var _a; return (_a = fileType.extensions) === null || _a === void 0 ? void 0 : _a.some(e => e.toLowerCase() === extension); });
77
+ if (claimed) {
78
+ continue;
79
+ }
80
+ docRegistry.addFileType({
81
+ name: `xtralab:file-type:${extension.slice(1)}`,
82
+ extensions: [extension],
83
+ icon,
84
+ contentType: 'file',
85
+ fileFormat: 'text'
86
+ });
87
+ }
88
+ }
89
+ /**
90
+ * Share the file browser's tree glyphs with tabs and git file lists.
91
+ */
92
+ const plugin = {
93
+ id: PLUGIN_ID,
94
+ description: "Gives the file browser's @pierre/trees glyphs to document tabs, the default file browser, and the git status panel.",
95
+ autoStart: true,
96
+ optional: [IDocumentWidgetOpener, IGitExtension],
97
+ activate: (app, opener, gitExtension) => {
98
+ const { docRegistry } = app;
99
+ for (const fileType of docRegistry.fileTypes()) {
100
+ applyTreeIcon(fileType);
101
+ }
102
+ registerExtraFileTypes(docRegistry);
103
+ // Pick up file types registered after this plugin activates.
104
+ docRegistry.changed.connect((_, args) => {
105
+ if (args.type === 'fileType' &&
106
+ args.change === 'added' &&
107
+ args.name !== undefined) {
108
+ const fileType = docRegistry.getFileType(args.name);
109
+ if (fileType !== undefined) {
110
+ applyTreeIcon(fileType);
111
+ }
112
+ }
113
+ });
114
+ if (opener !== null) {
115
+ opener.opened.connect((_, widget) => {
116
+ const setIcon = () => {
117
+ const icon = getSpecificTreeIcon(widget.context.path);
118
+ if (icon !== null) {
119
+ widget.title.icon = icon;
120
+ }
121
+ };
122
+ setIcon();
123
+ // Keep the tab icon right after a rename.
124
+ widget.context.pathChanged.connect(setIcon);
125
+ });
126
+ }
127
+ if (gitExtension !== null) {
128
+ gitExtension.statusChanged.connect((_, status) => {
129
+ for (const file of status.files) {
130
+ if (file.type === undefined) {
131
+ continue;
132
+ }
133
+ const icon = getSpecificTreeIcon(file.to);
134
+ if (icon === null) {
135
+ continue;
136
+ }
137
+ // Cross jupyterlab-git's nested `LabIcon` package boundary.
138
+ file.type = { ...file.type, icon };
139
+ }
140
+ });
141
+ }
142
+ }
143
+ };
144
+ export default plugin;
@@ -1,23 +1,18 @@
1
1
  import { JupyterFrontEnd } from '@jupyterlab/application';
2
- import { IThemeManager } from '@jupyterlab/apputils';
2
+ import { IThemeManager, MainAreaWidget } from '@jupyterlab/apputils';
3
3
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
4
  import { Contents } from '@jupyterlab/services';
5
- import { createGitDiffWidget, diffWidgetId } from './diffWidget';
5
+ import { ISignal } from '@lumino/signaling';
6
+ import { XtralabDiffWidget } from './diffWidget';
6
7
  import { IFileChange } from './tokens';
7
8
  /**
8
- * Command IDs exposed by the launcher's git diff path. The launcher
9
- * dashboard's "Changes" section drives {@link CommandIDs.openDiff} to open
10
- * its own diff tab, independent of `jupyterlab-git` (whose panel reaches
11
- * the same rendering via the providers in `diffProvider.tsx`).
9
+ * Command IDs exposed by the launcher's git diff path.
12
10
  */
13
11
  export declare namespace CommandIDs {
14
12
  const openDiff = "xtralab:git:open-diff";
15
13
  }
16
14
  /**
17
- * Argument shapes used by {@link CommandIDs.openDiff}. Cast to this type
18
- * inside the execute callback for type safety; the registry itself takes
19
- * `ReadonlyPartialJSONObject` because Lumino does not know about our
20
- * extension-specific shapes.
15
+ * Argument shapes used by {@link CommandIDs.openDiff}.
21
16
  */
22
17
  export declare namespace CommandArguments {
23
18
  interface IOpenDiff {
@@ -26,45 +21,51 @@ export declare namespace CommandArguments {
26
21
  pin?: boolean;
27
22
  }
28
23
  }
24
+ /**
25
+ * Widget id of the single, reused preview diff tab.
26
+ */
27
+ export declare const PREVIEW_DIFF_WIDGET_ID = "xtralab:diff:preview";
28
+ /**
29
+ * Deterministic widget id for a pinned file/group pair.
30
+ */
31
+ export declare function pinnedDiffWidgetId(change: IFileChange): string;
32
+ /**
33
+ * Launcher-owned host for a shared diff widget.
34
+ */
35
+ export declare class DiffMainAreaWidget extends MainAreaWidget<XtralabDiffWidget> {
36
+ constructor(options: MainAreaWidget.IOptions<XtralabDiffWidget>, change: IFileChange, pinned: boolean);
37
+ get change(): IFileChange;
38
+ get pinned(): boolean;
39
+ pin(): void;
40
+ get pinnedChanged(): ISignal<this, boolean>;
41
+ setChange(repoPath: string, change: IFileChange): void;
42
+ private _change;
43
+ private _pinned;
44
+ private _pinnedChanged;
45
+ }
29
46
  export interface IRegisterGitCommandsOptions {
30
47
  app: JupyterFrontEnd;
31
48
  themeManager: IThemeManager | null;
32
49
  /**
33
- * Contents manager used by the diff widget to write hunk-discard
34
- * results back to the working tree.
50
+ * Contents manager used to write hunk-discard results.
35
51
  */
36
52
  contentsManager: Contents.IManager;
37
53
  /**
38
- * Rendermime registry used by the notebook diff to render outputs and
39
- * markdown cells with their actual mime-type renderers (images as
40
- * images, HTML as HTML, etc.). May be `null` in stripped-down hosts —
41
- * the notebook diff falls back to a text representation in that case.
54
+ * Rendermime registry used by notebook diffs; may be `null`.
42
55
  */
43
56
  rendermime: IRenderMimeRegistry | null;
44
57
  /**
45
- * Called whenever the diff widget mutates the working tree (e.g. after
46
- * a hunk discard). The launcher dashboard polls git status on its own
47
- * cadence and `jupyterlab-git`'s panel auto-refreshes on the contents
48
- * `fileChanged` signal the save emits, so this is a hook for any extra
49
- * bookkeeping rather than a required refresh.
50
- */
51
- onChanged: () => void;
52
- /**
53
- * Called for every diff widget created via the {@link CommandIDs.openDiff}
54
- * command, before the widget is added to the shell. Lets the plugin track
55
- * the widget for layout restoration and reveal-on-reuse.
58
+ * Track newly created diff widgets before they are added to the shell.
56
59
  */
57
- trackDiff(widget: ReturnType<typeof createGitDiffWidget>): Promise<void>;
60
+ trackDiff(widget: DiffMainAreaWidget): Promise<void>;
58
61
  /**
59
- * Look up an already-open diff widget for a given file change. Returns
60
- * `undefined` when none is open.
62
+ * Look up an already-open diff widget for a file change.
61
63
  */
62
- findDiff(change: IFileChange, pin?: boolean): ReturnType<typeof createGitDiffWidget> | undefined;
63
- onPinned(widget: ReturnType<typeof createGitDiffWidget>): void;
64
+ findDiff(change: IFileChange, pin?: boolean): DiffMainAreaWidget | undefined;
65
+ onPinned(widget: DiffMainAreaWidget): void;
64
66
  }
65
67
  /**
66
68
  * Register the launcher's git diff command on the application command
67
69
  * registry.
68
70
  */
69
71
  export declare function registerGitCommands(options: IRegisterGitCommandsOptions): void;
70
- export { diffWidgetId };