xtralab 0.2.0 → 0.4.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 (39) hide show
  1. package/README.md +126 -125
  2. package/lib/editorBreadcrumbs/index.d.ts +11 -0
  3. package/lib/editorBreadcrumbs/index.js +41 -0
  4. package/lib/editorBreadcrumbs/widget.d.ts +34 -0
  5. package/lib/editorBreadcrumbs/widget.js +131 -0
  6. package/lib/editorIndent/index.d.ts +17 -0
  7. package/lib/editorIndent/index.js +142 -0
  8. package/lib/fileBrowser/commands.d.ts +5 -2
  9. package/lib/fileBrowser/commands.js +42 -3
  10. package/lib/fileBrowser/fileBrowser.js +394 -11
  11. package/lib/fileBrowser/gitStatus.d.ts +13 -0
  12. package/lib/fileBrowser/gitStatus.js +60 -0
  13. package/lib/fileBrowser/icons.d.ts +4 -4
  14. package/lib/fileBrowser/icons.js +4 -4
  15. package/lib/fileBrowser/widget.d.ts +50 -2
  16. package/lib/fileBrowser/widget.js +26 -2
  17. package/lib/git/icons.d.ts +1 -1
  18. package/lib/git/icons.js +1 -1
  19. package/lib/git/panelComponent.js +28 -11
  20. package/lib/index.js +4 -0
  21. package/lib/launcher/dashboard.js +78 -16
  22. package/package.json +22 -2
  23. package/src/editorBreadcrumbs/index.ts +70 -0
  24. package/src/editorBreadcrumbs/widget.ts +158 -0
  25. package/src/editorIndent/index.ts +177 -0
  26. package/src/fileBrowser/commands.ts +45 -3
  27. package/src/fileBrowser/fileBrowser.tsx +428 -12
  28. package/src/fileBrowser/gitStatus.ts +66 -0
  29. package/src/fileBrowser/icons.ts +4 -4
  30. package/src/fileBrowser/widget.tsx +80 -3
  31. package/src/git/icons.ts +1 -1
  32. package/src/git/panelComponent.tsx +29 -15
  33. package/src/index.ts +4 -0
  34. package/src/launcher/dashboard.tsx +135 -16
  35. package/style/editorBreadcrumbs.css +86 -0
  36. package/style/index.css +2 -0
  37. package/style/index.js +1 -0
  38. package/style/launcher.css +0 -5
  39. package/style/tabs.css +66 -0
@@ -0,0 +1,60 @@
1
+ import { expandStatusFiles, status } from '../git/api';
2
+ /**
3
+ * Translate a `jupyterlab_git` porcelain status into the closest
4
+ * `@pierre/trees` `GitStatus` enum value. The tree library only supports the
5
+ * six common statuses (added, deleted, ignored, modified, renamed, untracked)
6
+ * so the rare ones (unmerged, typechange, unknown) collapse onto `modified`
7
+ * — they all denote a pending change the user should see decorated.
8
+ */
9
+ function toTreeStatus(value) {
10
+ switch (value) {
11
+ case 'added':
12
+ case 'deleted':
13
+ case 'modified':
14
+ case 'renamed':
15
+ case 'untracked':
16
+ return value;
17
+ case 'unmerged':
18
+ case 'typechange':
19
+ case 'unknown':
20
+ default:
21
+ return 'modified';
22
+ }
23
+ }
24
+ /**
25
+ * Fetch the porcelain status for `repoPath` and convert it into a flat list
26
+ * of `GitStatusEntry` values consumable by `FileTree.setGitStatus`. Returns
27
+ * an empty array when the path is not in a git repo, the request fails, or
28
+ * the server reports a non-zero exit — the file browser must still work
29
+ * without git, so any error here is a no-op rather than a thrown exception.
30
+ *
31
+ * Files that have both a staged and an unstaged change are collapsed into a
32
+ * single entry: the unstaged status wins because that mirrors what is
33
+ * actually different on disk and matches VS Code's tree decoration behavior.
34
+ */
35
+ export async function loadGitStatusEntries(repoPath) {
36
+ try {
37
+ const result = await status(repoPath);
38
+ if (result.code !== 0) {
39
+ return [];
40
+ }
41
+ const changes = expandStatusFiles(result.files);
42
+ // Collapse staged + unstaged into one decoration per file. Prefer the
43
+ // unstaged group when both exist; otherwise keep whichever group is
44
+ // present.
45
+ const byPath = new Map();
46
+ for (const change of changes) {
47
+ if (change.group === 'unstaged' || !byPath.has(change.path)) {
48
+ byPath.set(change.path, change.status);
49
+ }
50
+ }
51
+ const entries = [];
52
+ byPath.forEach((value, path) => {
53
+ entries.push({ path, status: toTreeStatus(value) });
54
+ });
55
+ return entries;
56
+ }
57
+ catch (_a) {
58
+ return [];
59
+ }
60
+ }
@@ -1,9 +1,9 @@
1
1
  import { LabIcon } from '@jupyterlab/ui-components';
2
2
  import type { FileTreeIconConfig } from '@pierre/trees';
3
3
  /**
4
- * The glyph used as the Xtralab file browser's sidebar tab icon. Defined as a
4
+ * The glyph used as the xtralab file browser's sidebar tab icon. Defined as a
5
5
  * `LabIcon` (rather than the default `folderIcon` from
6
- * `@jupyterlab/ui-components`) so the Xtralab browser is visually distinguishable
6
+ * `@jupyterlab/ui-components`) so the xtralab browser is visually distinguishable
7
7
  * from JupyterLab's built-in file browser, which also lives in the left
8
8
  * sidebar.
9
9
  *
@@ -14,7 +14,7 @@ import type { FileTreeIconConfig } from '@pierre/trees';
14
14
  */
15
15
  export declare const xtralabFileBrowserIcon: LabIcon;
16
16
  /**
17
- * Icon configuration for the Xtralab file browser. Keeps the default `complete`
17
+ * Icon configuration for the xtralab file browser. Keeps the default `complete`
18
18
  * built-in icon set (the colored language icons) and layers a Jupyter notebook
19
19
  * symbol on top so `.ipynb` files render with the JupyterLab notebook glyph.
20
20
  *
@@ -24,7 +24,7 @@ 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
27
+ * Return a JupyterLab `LabIcon` matching the icon the xtralab file tree would
28
28
  * render for the given file path. Used by other plugins (e.g. the git diff
29
29
  * viewer) that want their main-area widget tabs to display the same per-file
30
30
  * glyph the tree shows in the sidebar.
@@ -2,9 +2,9 @@ var _a;
2
2
  import { LabIcon, fileIcon } from '@jupyterlab/ui-components';
3
3
  import { createFileTreeIconResolver, getBuiltInFileIconColor, getBuiltInSpriteSheet } from '@pierre/trees';
4
4
  /**
5
- * The glyph used as the Xtralab file browser's sidebar tab icon. Defined as a
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
7
- * `@jupyterlab/ui-components`) so the Xtralab browser is visually distinguishable
7
+ * `@jupyterlab/ui-components`) so the xtralab browser is visually distinguishable
8
8
  * from JupyterLab's built-in file browser, which also lives in the left
9
9
  * sidebar.
10
10
  *
@@ -42,7 +42,7 @@ const JUPYTER_NOTEBOOK_SPRITE_SHEET = `<svg data-icon-sprite aria-hidden="true"
42
42
  </symbol>
43
43
  </svg>`;
44
44
  /**
45
- * Icon configuration for the Xtralab file browser. Keeps the default `complete`
45
+ * Icon configuration for the xtralab file browser. Keeps the default `complete`
46
46
  * built-in icon set (the colored language icons) and layers a Jupyter notebook
47
47
  * symbol on top so `.ipynb` files render with the JupyterLab notebook glyph.
48
48
  *
@@ -105,7 +105,7 @@ function extractSymbol(symbolId) {
105
105
  };
106
106
  }
107
107
  /**
108
- * Return a JupyterLab `LabIcon` matching the icon the Xtralab file tree would
108
+ * Return a JupyterLab `LabIcon` matching the icon the xtralab file tree would
109
109
  * render for the given file path. Used by other plugins (e.g. the git diff
110
110
  * viewer) that want their main-area widget tabs to display the same per-file
111
111
  * glyph the tree shows in the sidebar.
@@ -5,7 +5,7 @@ import { ISignal } from '@lumino/signaling';
5
5
  import { Widget } from '@lumino/widgets';
6
6
  export declare const FILE_BROWSER_ID = "xtralab:file-browser";
7
7
  /**
8
- * The CSS class added to the Xtralab file browser widget. The selectors that
8
+ * The CSS class added to the xtralab file browser widget. The selectors that
9
9
  * bind application context menu items hang off this class, so it must remain
10
10
  * specific enough not to clash with other browsers (such as the default
11
11
  * `jp-FileBrowser`).
@@ -17,7 +17,7 @@ export interface IXtralabFileBrowserOptions {
17
17
  onOpenFile?: (serverPath: string) => void;
18
18
  }
19
19
  /**
20
- * Public API of the Xtralab file browser used by command handlers and other
20
+ * Public API of the xtralab file browser used by command handlers and other
21
21
  * collaborators. Kept narrow on purpose so the React component stays free to
22
22
  * evolve the underlying tree integration.
23
23
  */
@@ -56,6 +56,28 @@ export interface IXtralabFileBrowser {
56
56
  * sees the new item without a full refresh.
57
57
  */
58
58
  readonly pathAdded: ISignal<IXtralabFileBrowser, string>;
59
+ /**
60
+ * Emits when an external caller asks the tree to scroll to and select
61
+ * the given canonical path. The React component listens, lazily loads
62
+ * any unloaded ancestor directories, expands them, and selects the
63
+ * target. Used by the editor breadcrumbs to jump back to a file or
64
+ * folder shown in the breadcrumb trail.
65
+ */
66
+ readonly revealRequested: ISignal<IXtralabFileBrowser, string>;
67
+ /**
68
+ * Emits when an external caller asks the tree to return to the
69
+ * workspace root: clear any current selection and scroll back to the
70
+ * first row. Distinct from {@link revealRequested} because the
71
+ * workspace root has no tree row of its own — it cannot be reached
72
+ * by passing a path.
73
+ */
74
+ readonly rootRequested: ISignal<IXtralabFileBrowser, void>;
75
+ /**
76
+ * Emits when an external caller asks the tree to collapse every
77
+ * expanded folder. The React component listens and walks the loaded
78
+ * directories, calling `.collapse()` on each expanded one.
79
+ */
80
+ readonly collapseAllRequested: ISignal<IXtralabFileBrowser, void>;
59
81
  /** Trigger a refresh of every loaded directory in the tree. */
60
82
  refresh(): void;
61
83
  /**
@@ -64,6 +86,23 @@ export interface IXtralabFileBrowser {
64
86
  * convention: directories carry a trailing slash, files do not.
65
87
  */
66
88
  notifyPathAdded(canonicalPath: string): void;
89
+ /**
90
+ * Ask the tree to reveal {@link canonicalPath}: load and expand any
91
+ * unloaded ancestor directories, then select and scroll the target
92
+ * into view. `canonicalPath` follows the `@pierre/trees` convention
93
+ * (directories carry a trailing slash, files do not). Must be a
94
+ * non-empty path — use {@link scrollToRoot} for the root gesture.
95
+ */
96
+ reveal(canonicalPath: string): void;
97
+ /**
98
+ * Ask the tree to return to the workspace root: clear any current
99
+ * selection and scroll back to the top of the tree.
100
+ */
101
+ scrollToRoot(): void;
102
+ /**
103
+ * Ask the tree to collapse every currently expanded folder.
104
+ */
105
+ collapseAll(): void;
67
106
  }
68
107
  /**
69
108
  * Lumino widget that hosts the React-based `@pierre/trees` file browser.
@@ -78,6 +117,9 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
78
117
  get selectionChanged(): ISignal<this, readonly string[]>;
79
118
  get refreshRequested(): ISignal<this, void>;
80
119
  get pathAdded(): ISignal<this, string>;
120
+ get revealRequested(): ISignal<this, string>;
121
+ get rootRequested(): ISignal<this, void>;
122
+ get collapseAllRequested(): ISignal<this, void>;
81
123
  /**
82
124
  * Update the cached selection. Called from the React tree when the
83
125
  * underlying `@pierre/trees` model emits a selection change.
@@ -85,6 +127,9 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
85
127
  updateSelection(paths: readonly string[]): void;
86
128
  refresh(): void;
87
129
  notifyPathAdded(canonicalPath: string): void;
130
+ reveal(canonicalPath: string): void;
131
+ scrollToRoot(): void;
132
+ collapseAll(): void;
88
133
  private _contentsManager;
89
134
  private _docManager;
90
135
  private _onOpenFile;
@@ -92,6 +137,9 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
92
137
  private _selectionChanged;
93
138
  private _refreshRequested;
94
139
  private _pathAdded;
140
+ private _revealRequested;
141
+ private _rootRequested;
142
+ private _collapseAllRequested;
95
143
  private _toolbar;
96
144
  private _content;
97
145
  }
@@ -6,7 +6,7 @@ import { FileBrowserComponent } from './fileBrowser';
6
6
  import { xtralabFileBrowserIcon } from './icons';
7
7
  export const FILE_BROWSER_ID = 'xtralab:file-browser';
8
8
  /**
9
- * The CSS class added to the Xtralab file browser widget. The selectors that
9
+ * The CSS class added to the xtralab file browser widget. The selectors that
10
10
  * bind application context menu items hang off this class, so it must remain
11
11
  * specific enough not to clash with other browsers (such as the default
12
12
  * `jp-FileBrowser`).
@@ -26,13 +26,16 @@ export class XtralabFileBrowser extends Widget {
26
26
  this._selectionChanged = new Signal(this);
27
27
  this._refreshRequested = new Signal(this);
28
28
  this._pathAdded = new Signal(this);
29
+ this._revealRequested = new Signal(this);
30
+ this._rootRequested = new Signal(this);
31
+ this._collapseAllRequested = new Signal(this);
29
32
  const layout = (this.layout = new PanelLayout());
30
33
  this._contentsManager = options.contentsManager;
31
34
  this._docManager = options.docManager;
32
35
  this._onOpenFile = options.onOpenFile;
33
36
  this.id = FILE_BROWSER_ID;
34
37
  this.title.icon = xtralabFileBrowserIcon;
35
- this.title.caption = 'Xtralab File Browser';
38
+ this.title.caption = 'xtralab File Browser';
36
39
  this.addClass(FILE_BROWSER_CSS_CLASS);
37
40
  this._toolbar = new Toolbar();
38
41
  this._toolbar.addClass(TOOLBAR_CSS_CLASS);
@@ -65,6 +68,15 @@ export class XtralabFileBrowser extends Widget {
65
68
  get pathAdded() {
66
69
  return this._pathAdded;
67
70
  }
71
+ get revealRequested() {
72
+ return this._revealRequested;
73
+ }
74
+ get rootRequested() {
75
+ return this._rootRequested;
76
+ }
77
+ get collapseAllRequested() {
78
+ return this._collapseAllRequested;
79
+ }
68
80
  /**
69
81
  * Update the cached selection. Called from the React tree when the
70
82
  * underlying `@pierre/trees` model emits a selection change.
@@ -79,6 +91,18 @@ export class XtralabFileBrowser extends Widget {
79
91
  notifyPathAdded(canonicalPath) {
80
92
  this._pathAdded.emit(canonicalPath);
81
93
  }
94
+ reveal(canonicalPath) {
95
+ if (canonicalPath.length === 0) {
96
+ throw new Error('XtralabFileBrowser.reveal requires a non-empty canonical path; use scrollToRoot for the workspace root.');
97
+ }
98
+ this._revealRequested.emit(canonicalPath);
99
+ }
100
+ scrollToRoot() {
101
+ this._rootRequested.emit();
102
+ }
103
+ collapseAll() {
104
+ this._collapseAllRequested.emit();
105
+ }
82
106
  }
83
107
  /**
84
108
  * The React-rendered region of the file browser. Lives inside the host
@@ -5,7 +5,7 @@ import { LabIcon } from '@jupyterlab/ui-components';
5
5
  * recoloring and inverse-color sidebar painting automatically.
6
6
  *
7
7
  * Source: `git-branch` from SVG Repo (https://www.svgrepo.com/svg/377352/git-branch),
8
- * part of the same Pixelarticons family as the Xtralab file browser's `file-tree`
8
+ * part of the same Pixelarticons family as the xtralab file browser's `file-tree`
9
9
  * icon, so the two sidebar tab glyphs read as a matched set. The original
10
10
  * `fill="#000000"` is replaced with `fill="currentColor"` and the root element
11
11
  * is given the `jp-icon3` class so the icon picks up JupyterLab's theme color
package/lib/git/icons.js CHANGED
@@ -5,7 +5,7 @@ import { LabIcon } from '@jupyterlab/ui-components';
5
5
  * recoloring and inverse-color sidebar painting automatically.
6
6
  *
7
7
  * Source: `git-branch` from SVG Repo (https://www.svgrepo.com/svg/377352/git-branch),
8
- * part of the same Pixelarticons family as the Xtralab file browser's `file-tree`
8
+ * part of the same Pixelarticons family as the xtralab file browser's `file-tree`
9
9
  * icon, so the two sidebar tab glyphs read as a matched set. The original
10
10
  * `fill="#000000"` is replaced with `fill="currentColor"` and the root element
11
11
  * is given the `jp-icon3` class so the icon picks up JupyterLab's theme color
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import { Poll } from '@lumino/polling';
2
3
  import { add, addAll, checkout, porcelainToFileChanges, reset, resetAll, showTopLevel, status } from './api';
3
4
  /**
4
5
  * The polling interval for the panel. We refresh the status periodically so
@@ -6,6 +7,13 @@ import { add, addAll, checkout, porcelainToFileChanges, reset, resetAll, showTop
6
7
  * visible without requiring a manual refresh.
7
8
  */
8
9
  const STATUS_POLL_INTERVAL_MS = 5000;
10
+ /**
11
+ * Upper bound on the poll's exponential backoff when status requests fail
12
+ * repeatedly. Matches the default JupyterLab file browser's `max`: long
13
+ * enough that a server outage stops hammering the API, short enough that
14
+ * a transient failure heals on its own.
15
+ */
16
+ const STATUS_POLL_MAX_MS = 300000;
9
17
  /**
10
18
  * Top-level React component rendered inside the {@link GitPanel} widget.
11
19
  * Owns the polling + refresh wiring around the `/git/status` endpoint and
@@ -59,23 +67,32 @@ export function GitPanelComponent(props) {
59
67
  });
60
68
  }
61
69
  }, [repoPath]);
62
- // Initial load + polling. `refresh` is stable for a given `repoPath`, so
63
- // the polling interval is only re-created when the repo path changes.
64
- React.useEffect(() => {
65
- void refresh();
66
- const interval = window.setInterval(() => void refresh(), STATUS_POLL_INTERVAL_MS);
67
- return () => {
68
- window.clearInterval(interval);
69
- };
70
- }, [refresh]);
71
- // External refresh triggers (toolbar button, plugin-level events).
70
+ // Initial load + polling. Driven by a Lumino `Poll` so the panel stops
71
+ // hitting the git endpoint while the JupyterLab tab is hidden, and so
72
+ // repeated failures back off exponentially instead of spinning at 5s.
73
+ // External refresh triggers (toolbar button, plugin-level events) call
74
+ // `poll.refresh()` to schedule an immediate tick instead of running
75
+ // `refresh()` directly — that keeps the panel from issuing two parallel
76
+ // status requests when an automatic tick happens to land at the same
77
+ // time as an external trigger.
72
78
  React.useEffect(() => {
79
+ const poll = new Poll({
80
+ name: '@xtralab/git:panelStatus',
81
+ factory: () => refresh(),
82
+ frequency: {
83
+ interval: STATUS_POLL_INTERVAL_MS,
84
+ backoff: true,
85
+ max: STATUS_POLL_MAX_MS
86
+ },
87
+ standby: 'when-hidden'
88
+ });
73
89
  const handler = () => {
74
- void refresh();
90
+ void poll.refresh();
75
91
  };
76
92
  refreshSignal.connect(handler);
77
93
  return () => {
78
94
  refreshSignal.disconnect(handler);
95
+ poll.dispose();
79
96
  };
80
97
  }, [refresh, refreshSignal]);
81
98
  const runAction = React.useCallback(async (change, action, run) => {
package/lib/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import fileBrowserPlugin from './fileBrowser';
2
+ import editorBreadcrumbsPlugin from './editorBreadcrumbs';
3
+ import editorIndentPlugin from './editorIndent';
2
4
  import gitPlugin from './git';
3
5
  import launcherPlugin from './launcher';
4
6
  import menusPlugin from './menus';
@@ -12,6 +14,8 @@ import statusBarPlugin from './statusBar';
12
14
  * available end up running.
13
15
  */
14
16
  const plugins = [
17
+ editorBreadcrumbsPlugin,
18
+ editorIndentPlugin,
15
19
  fileBrowserPlugin,
16
20
  gitPlugin,
17
21
  launcherPlugin,
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
- import { LabIcon, ReactWidget, refreshIcon } from '@jupyterlab/ui-components';
2
+ import { Poll } from '@lumino/polling';
3
+ import { LabIcon, ReactWidget, consoleIcon, notebookIcon, refreshIcon, terminalIcon } from '@jupyterlab/ui-components';
3
4
  import { expandStatusFiles, status } from '../git/api';
4
5
  import { CommandIDs as GitCommandIDs } from '../git/commands';
5
6
  import { agentCommandId } from './commands';
@@ -24,6 +25,12 @@ export class LauncherDashboard extends ReactWidget {
24
25
  * only opened intentionally, and the grid does not need to be live.
25
26
  */
26
27
  const CHANGES_POLL_INTERVAL_MS = 8000;
28
+ /**
29
+ * Upper bound on the exponential backoff when the status request fails
30
+ * repeatedly. Matches the rest of the plugin's polls so behavior is
31
+ * consistent across the dashboard, the git panel, and the file tree.
32
+ */
33
+ const CHANGES_POLL_MAX_MS = 300000;
27
34
  function LauncherDashboardComponent(props) {
28
35
  const { commands, agents, onAgentLaunch, repoPath, cwd } = props;
29
36
  const [prompt, setPrompt] = React.useState('');
@@ -45,12 +52,23 @@ function LauncherDashboardComponent(props) {
45
52
  });
46
53
  }
47
54
  }, [repoPath]);
55
+ // Driven by a Lumino `Poll` so the dashboard stops hitting the git
56
+ // endpoint while the JupyterLab tab is hidden, and so repeated
57
+ // failures back off exponentially instead of pegging the 8s cadence.
48
58
  React.useEffect(() => {
49
- void refresh();
50
- const handle = window.setInterval(() => {
51
- void refresh();
52
- }, CHANGES_POLL_INTERVAL_MS);
53
- return () => window.clearInterval(handle);
59
+ const poll = new Poll({
60
+ name: '@xtralab/launcher:gitChanges',
61
+ factory: () => refresh(),
62
+ frequency: {
63
+ interval: CHANGES_POLL_INTERVAL_MS,
64
+ backoff: true,
65
+ max: CHANGES_POLL_MAX_MS
66
+ },
67
+ standby: 'when-hidden'
68
+ });
69
+ return () => {
70
+ poll.dispose();
71
+ };
54
72
  }, [refresh]);
55
73
  const launch = React.useCallback(async (agent) => {
56
74
  const args = { cwd };
@@ -63,12 +81,40 @@ function LauncherDashboardComponent(props) {
63
81
  onAgentLaunch(result);
64
82
  }
65
83
  }, [commands, cwd, prompt, onAgentLaunch]);
84
+ const launchTerminal = React.useCallback(async () => {
85
+ const result = (await commands.execute('terminal:create-new', {
86
+ cwd
87
+ }));
88
+ if (result) {
89
+ onAgentLaunch(result);
90
+ }
91
+ }, [commands, cwd, onAgentLaunch]);
92
+ const launchNotebook = React.useCallback(async () => {
93
+ // No `kernelName` — let the notebook extension fall back to the default
94
+ // kernel or prompt the user, matching the stock launcher's notebook
95
+ // tile behavior.
96
+ const result = (await commands.execute('notebook:create-new', {
97
+ cwd
98
+ }));
99
+ if (result) {
100
+ onAgentLaunch(result);
101
+ }
102
+ }, [commands, cwd, onAgentLaunch]);
103
+ const launchConsole = React.useCallback(async () => {
104
+ const result = (await commands.execute('console:create', {
105
+ cwd
106
+ }));
107
+ if (result) {
108
+ onAgentLaunch(result);
109
+ }
110
+ }, [commands, cwd, onAgentLaunch]);
66
111
  const openDiff = React.useCallback((change) => {
67
112
  const args = { repoPath, change };
68
113
  void commands.execute(GitCommandIDs.openDiff, args);
69
114
  }, [commands, repoPath]);
70
115
  return (React.createElement("div", { className: "jp-xtralab-Launcher-body" },
71
116
  React.createElement(AgentSection, { agents: agents, prompt: prompt, onPromptChange: setPrompt, onLaunch: launch }),
117
+ React.createElement(OpenSection, { onLaunchTerminal: launchTerminal, onLaunchNotebook: launchNotebook, onLaunchConsole: launchConsole }),
72
118
  React.createElement(ChangesSection, { git: git, onOpen: openDiff, onRefresh: () => void refresh() })));
73
119
  }
74
120
  function ChangesSection(props) {
@@ -126,10 +172,9 @@ function AgentSection(props) {
126
172
  const { agents, prompt, onPromptChange, onLaunch } = props;
127
173
  const trimmed = prompt.trim();
128
174
  const promptActive = trimmed.length > 0;
129
- // The "primary" agent is the first prompt-capable one in the rendered
130
- // order. We mark it visually so the keyboard shortcut target isn't a
131
- // mystery, and reuse the same agent when Cmd/Ctrl+Enter fires from the
132
- // textarea — picking by the same rule keeps the two behaviors in sync.
175
+ // The first prompt-capable agent is what Cmd/Ctrl+Enter fires; the hint
176
+ // text below the textarea names it so the keyboard shortcut target
177
+ // isn't a mystery.
133
178
  const primaryAgent = agents.find(agent => agent.promptArgs !== undefined);
134
179
  const hintId = 'jp-xtralab-Launcher-agent-hint';
135
180
  return (React.createElement("section", { className: "jp-xtralab-Launcher-section" },
@@ -149,16 +194,33 @@ function AgentSection(props) {
149
194
  React.createElement("div", { className: "jp-xtralab-Launcher-agents" }, agents.map(agent => {
150
195
  const supportsPrompt = agent.promptArgs !== undefined;
151
196
  const disabled = promptActive && !supportsPrompt;
152
- const isPrimary = agent === primaryAgent;
153
197
  const tooltip = disabled
154
198
  ? `${agent.label} doesn't accept an initial prompt — clear the prompt to launch.`
155
199
  : agent.caption;
156
- const classes = ['jp-xtralab-Launcher-agent'];
157
- if (isPrimary) {
158
- classes.push('jp-xtralab-Launcher-agent-primary');
159
- }
160
- return (React.createElement("button", { key: agent.id, type: "button", className: classes.join(' '), title: tooltip, "aria-label": agent.label, "aria-describedby": hintId, disabled: disabled, onClick: () => onLaunch(agent) },
200
+ return (React.createElement("button", { key: agent.id, type: "button", className: "jp-xtralab-Launcher-agent", title: tooltip, "aria-label": agent.label, "aria-describedby": hintId, disabled: disabled, onClick: () => onLaunch(agent) },
161
201
  React.createElement(LabIcon.resolveReact, { icon: agent.icon, tag: "span", className: "jp-xtralab-Launcher-agent-icon" }),
162
202
  React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, agent.label)));
163
203
  }))));
164
204
  }
205
+ /**
206
+ * The non-agent launch tiles — a plain terminal, a new notebook, and a
207
+ * new console. They sit in a separate section below the agent grid so
208
+ * they don't compete with the AI agents visually, and they're always
209
+ * available regardless of the agent prompt textarea (which doesn't
210
+ * apply to them).
211
+ */
212
+ function OpenSection(props) {
213
+ const { onLaunchTerminal, onLaunchNotebook, onLaunchConsole } = props;
214
+ return (React.createElement("section", { className: "jp-xtralab-Launcher-section" },
215
+ React.createElement("h2", { className: "jp-xtralab-Launcher-section-title" }, "Open"),
216
+ React.createElement("div", { className: "jp-xtralab-Launcher-agents" },
217
+ React.createElement("button", { type: "button", className: "jp-xtralab-Launcher-agent", title: "Open a new terminal.", "aria-label": "Open a new terminal", onClick: onLaunchTerminal },
218
+ React.createElement(LabIcon.resolveReact, { icon: terminalIcon, tag: "span", className: "jp-xtralab-Launcher-agent-icon" }),
219
+ React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, "Terminal")),
220
+ React.createElement("button", { type: "button", className: "jp-xtralab-Launcher-agent", title: "Create a new notebook.", "aria-label": "Create a new notebook", onClick: onLaunchNotebook },
221
+ React.createElement(LabIcon.resolveReact, { icon: notebookIcon, tag: "span", className: "jp-xtralab-Launcher-agent-icon" }),
222
+ React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, "Notebook")),
223
+ React.createElement("button", { type: "button", className: "jp-xtralab-Launcher-agent", title: "Create a new console.", "aria-label": "Create a new console", onClick: onLaunchConsole },
224
+ React.createElement(LabIcon.resolveReact, { icon: consoleIcon, tag: "span", className: "jp-xtralab-Launcher-agent-icon" }),
225
+ React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, "Console")))));
226
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtralab",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser and a VS Code-style git changes panel, and applies a quieter default workspace configuration.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -57,8 +57,12 @@
57
57
  "watch:labextension": "jupyter-builder watch ."
58
58
  },
59
59
  "dependencies": {
60
+ "@codemirror/language": "^6.12.1",
61
+ "@codemirror/state": "^6.5.4",
62
+ "@codemirror/view": "^6.39.14",
60
63
  "@jupyterlab/application": "^4.6.0-alpha.5",
61
64
  "@jupyterlab/apputils": "^4.7.0-alpha.5",
65
+ "@jupyterlab/codemirror": "^4.6.0-alpha.5",
62
66
  "@jupyterlab/coreutils": "^6.6.0-alpha.5",
63
67
  "@jupyterlab/docmanager": "^4.6.0-alpha.5",
64
68
  "@jupyterlab/docregistry": "^4.6.0-alpha.5",
@@ -78,10 +82,12 @@
78
82
  "@lumino/disposable": "^2.0.0",
79
83
  "@lumino/dragdrop": "^2.0.0",
80
84
  "@lumino/messaging": "^2.0.0",
85
+ "@lumino/polling": "^2.1.5",
81
86
  "@lumino/signaling": "^2.0.0",
82
87
  "@lumino/widgets": "^2.0.0",
83
88
  "@pierre/diffs": "^1.1.20",
84
89
  "@pierre/trees": "1.0.0-beta.3",
90
+ "detect-indent": "^7.0.2",
85
91
  "ignore": "^5.3.2",
86
92
  "react": "^18.2.0",
87
93
  "react-dom": "^18.2.0"
@@ -145,12 +151,26 @@
145
151
  ],
146
152
  "parser": "@typescript-eslint/parser",
147
153
  "parserOptions": {
148
- "project": "tsconfig.json",
154
+ "project": [
155
+ "tsconfig.json",
156
+ "desktop/tsconfig.json",
157
+ "desktop/tsconfig.forge.json"
158
+ ],
149
159
  "sourceType": "module"
150
160
  },
151
161
  "plugins": [
152
162
  "@typescript-eslint"
153
163
  ],
164
+ "overrides": [
165
+ {
166
+ "files": [
167
+ "desktop/src/**/*.ts"
168
+ ],
169
+ "rules": {
170
+ "@typescript-eslint/naming-convention": "off"
171
+ }
172
+ }
173
+ ],
154
174
  "rules": {
155
175
  "@typescript-eslint/naming-convention": [
156
176
  "error",
@@ -0,0 +1,70 @@
1
+ import {
2
+ JupyterFrontEnd,
3
+ JupyterFrontEndPlugin
4
+ } from '@jupyterlab/application';
5
+ import type {
6
+ DocumentRegistry,
7
+ IDocumentWidget
8
+ } from '@jupyterlab/docregistry';
9
+ import type { CommandRegistry } from '@lumino/commands';
10
+ import { DisposableDelegate, type IDisposable } from '@lumino/disposable';
11
+ import type { Widget } from '@lumino/widgets';
12
+
13
+ import { EditorBreadcrumbs } from './widget';
14
+
15
+ const PLUGIN_ID = 'xtralab:editor-breadcrumbs';
16
+ const EDITOR_FACTORY = 'Editor';
17
+ const TOOLBAR_ITEM_NAME = 'xtralab-editor-breadcrumbs';
18
+
19
+ class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
20
+ IDocumentWidget<Widget, DocumentRegistry.IModel>,
21
+ DocumentRegistry.IModel
22
+ > {
23
+ constructor(commands: CommandRegistry) {
24
+ this._commands = commands;
25
+ }
26
+
27
+ createNew(
28
+ widget: IDocumentWidget<Widget, DocumentRegistry.IModel>,
29
+ context: DocumentRegistry.IContext<DocumentRegistry.IModel>
30
+ ): IDisposable | void {
31
+ const breadcrumbs = new EditorBreadcrumbs({
32
+ context,
33
+ commands: this._commands
34
+ });
35
+ const added = widget.toolbar.insertItem(0, TOOLBAR_ITEM_NAME, breadcrumbs);
36
+
37
+ if (!added) {
38
+ breadcrumbs.dispose();
39
+ return;
40
+ }
41
+
42
+ return new DisposableDelegate(() => {
43
+ breadcrumbs.dispose();
44
+ });
45
+ }
46
+
47
+ private _commands: CommandRegistry;
48
+ }
49
+
50
+ /**
51
+ * Adds a VS Code-style path breadcrumb to text editor toolbars. Each
52
+ * segment is clickable: clicking dispatches `xtralab:reveal-path` so
53
+ * any plugin that listens (today, the file browser) can surface the
54
+ * underlying folder or file. The breadcrumbs plugin therefore does not
55
+ * import the file browser at all — the JupyterLab command registry is
56
+ * the only seam between them.
57
+ */
58
+ const plugin: JupyterFrontEndPlugin<void> = {
59
+ id: PLUGIN_ID,
60
+ description: 'Display the active file path in text editor toolbars.',
61
+ autoStart: true,
62
+ activate: (app: JupyterFrontEnd): void => {
63
+ app.docRegistry.addWidgetExtension(
64
+ EDITOR_FACTORY,
65
+ new EditorBreadcrumbsExtension(app.commands)
66
+ );
67
+ }
68
+ };
69
+
70
+ export default plugin;