xtralab 0.1.2 → 0.3.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 (75) hide show
  1. package/README.md +129 -46
  2. package/lib/fileBrowser/commands.d.ts +2 -2
  3. package/lib/fileBrowser/commands.js +2 -2
  4. package/lib/fileBrowser/fileBrowser.js +72 -2
  5. package/lib/fileBrowser/gitignore.d.ts +29 -0
  6. package/lib/fileBrowser/gitignore.js +74 -0
  7. package/lib/fileBrowser/icons.d.ts +4 -4
  8. package/lib/fileBrowser/icons.js +4 -4
  9. package/lib/fileBrowser/widget.d.ts +2 -2
  10. package/lib/fileBrowser/widget.js +2 -2
  11. package/lib/git/api.d.ts +11 -0
  12. package/lib/git/api.js +19 -0
  13. package/lib/git/commands.d.ts +3 -1
  14. package/lib/git/commands.js +10 -4
  15. package/lib/git/diffWidget.d.ts +11 -0
  16. package/lib/git/diffWidget.js +90 -9
  17. package/lib/git/icons.d.ts +1 -1
  18. package/lib/git/icons.js +1 -1
  19. package/lib/git/index.js +22 -6
  20. package/lib/git/panelComponent.d.ts +3 -1
  21. package/lib/git/panelComponent.js +6 -3
  22. package/lib/index.js +9 -1
  23. package/lib/launcher/agents.d.ts +78 -0
  24. package/lib/launcher/agents.js +168 -0
  25. package/lib/launcher/availability.d.ts +12 -0
  26. package/lib/launcher/availability.js +53 -0
  27. package/lib/launcher/commands.d.ts +22 -0
  28. package/lib/launcher/commands.js +89 -0
  29. package/lib/launcher/dashboard.d.ts +46 -0
  30. package/lib/launcher/dashboard.js +214 -0
  31. package/lib/launcher/icons.d.ts +33 -0
  32. package/lib/launcher/icons.js +113 -0
  33. package/lib/launcher/index.d.ts +23 -0
  34. package/lib/launcher/index.js +191 -0
  35. package/lib/launcher/invocation.d.ts +29 -0
  36. package/lib/launcher/invocation.js +42 -0
  37. package/lib/menus/index.d.ts +14 -0
  38. package/lib/menus/index.js +102 -0
  39. package/lib/sidebar/index.d.ts +18 -0
  40. package/lib/sidebar/index.js +155 -0
  41. package/lib/statusBar/index.d.ts +14 -0
  42. package/lib/statusBar/index.js +89 -0
  43. package/lib/statusBar/widget.d.ts +121 -0
  44. package/lib/statusBar/widget.js +237 -0
  45. package/package.json +25 -3
  46. package/schema/launcher.json +70 -0
  47. package/schema/sidebar.json +57 -0
  48. package/src/fileBrowser/commands.ts +2 -2
  49. package/src/fileBrowser/fileBrowser.tsx +76 -2
  50. package/src/fileBrowser/gitignore.ts +86 -0
  51. package/src/fileBrowser/icons.ts +4 -4
  52. package/src/fileBrowser/widget.tsx +3 -3
  53. package/src/git/api.ts +20 -0
  54. package/src/git/commands.ts +19 -5
  55. package/src/git/diffWidget.tsx +109 -13
  56. package/src/git/icons.ts +1 -1
  57. package/src/git/index.ts +31 -5
  58. package/src/git/panelComponent.tsx +17 -5
  59. package/src/index.ts +9 -1
  60. package/src/launcher/agents.ts +248 -0
  61. package/src/launcher/availability.ts +73 -0
  62. package/src/launcher/commands.ts +103 -0
  63. package/src/launcher/dashboard.tsx +462 -0
  64. package/src/launcher/icons.ts +123 -0
  65. package/src/launcher/index.ts +228 -0
  66. package/src/launcher/invocation.ts +45 -0
  67. package/src/menus/index.ts +124 -0
  68. package/src/sidebar/index.ts +195 -0
  69. package/src/statusBar/index.ts +115 -0
  70. package/src/statusBar/widget.tsx +318 -0
  71. package/style/git.css +4 -8
  72. package/style/index.css +2 -0
  73. package/style/index.js +2 -0
  74. package/style/launcher.css +323 -0
  75. package/style/statusBar.css +95 -0
package/README.md CHANGED
@@ -2,70 +2,153 @@
2
2
 
3
3
  An opinionated JupyterLab meta-package.
4
4
 
5
- `xtralab` bundles a curated set of JupyterLab extensions, ships its own
6
- JupyterLab extension with a path-first file browser and a VS Code-style git
7
- changes panel, and applies a quieter default workspace configuration. The
8
- defaults are shipped under `etc/jupyter/labconfig` and the bundled
9
- labextension is shipped under `share/jupyter/labextensions/xtralab`.
5
+ Bundles a curated set of extensions, a path-first file browser, a VS Code-style
6
+ git changes panel, an agent-focused launcher, and a quieter default workspace.
10
7
 
11
- The goal is to keep the package easy to inspect: most behavior comes from the
12
- upstream Jupyter packages, while `xtralab` defines the default environment we
13
- want out of the box.
8
+ ## Install
14
9
 
15
- ## Approach
10
+ ```bash
11
+ pip install xtralab
12
+ ```
16
13
 
17
- The package brings together:
14
+ ## Usage
18
15
 
19
- - The `ajlab` meta-package (agent-ready JupyterLab — collaboration plumbing, MCP support, command tooling, and its own quieter defaults)
20
- - JupyterLab 4.6+
21
- - Git server integration (`jupyterlab-git` — backs the bundled changes panel)
22
- - Quick file opening (`jupyterlab-quickopen`)
23
- - Cursor styling helpers (`jupyterlab-cursor-light`, `jupyterlab-cursor-theme`)
24
- - Day and night themes (`jupyterlab-day`, `jupyterlab-night`)
16
+ ### As a JupyterLab package
25
17
 
26
- These are installed as normal Python dependencies, so deployments can still
27
- override versions and Jupyter configuration in the usual ways.
18
+ Run JupyterLab the usual way:
28
19
 
29
- ## Bundled labextension
20
+ ```bash
21
+ jupyter lab
22
+ ```
30
23
 
31
- `xtralab` ships its own prebuilt labextension that contributes:
24
+ ### As a desktop app
32
25
 
33
- - A path-first file browser in the left sidebar, built on `@pierre/trees`.
34
- - A VS Code-style "Source Control" panel in the left sidebar, powered by the
35
- `jupyterlab-git` server REST API and `@pierre/diffs`. The bundled
36
- `@jupyterlab/git` frontend is disabled automatically so the two panels do
37
- not coexist.
26
+ A standalone Electron build (DMG on macOS, AppImage on Linux) ships with each
27
+ tagged release — grab the installer for your platform from the
28
+ [Releases page](https://github.com/jtpio/xtralab/releases/latest). Builds from
29
+ the current `main` branch are also produced on every push as workflow
30
+ artifacts under the repository's
31
+ [Actions tab](https://github.com/jtpio/xtralab/actions). See
32
+ [CONTRIBUTING.md](./CONTRIBUTING.md) for the architecture and local build
33
+ instructions.
38
34
 
39
- ## Default settings
35
+ ## What's included
40
36
 
41
- `xtralab` ships the following JupyterLab defaults:
37
+ - [`ajlab`](https://github.com/jtpio/ajlab) — agent-ready JupyterLab base
38
+ - JupyterLab 4.6+
39
+ - `jupyterlab-git` — backs the bundled changes panel
40
+ - `jupyterlab-lsp` + `ty` — Python LSP via Astral's `ty` (bundled); also
41
+ detects `typescript-language-server` on `PATH` for JS/TS
42
+ - `jupyterlab-quickopen`
43
+ - `jupyterlab-cursor-light`, `jupyterlab-cursor-dark`
44
+ - `jupyterlab-day`, `jupyterlab-night` themes
45
+
46
+ The bundled labextension adds:
47
+
48
+ - A path-first file browser in the left sidebar.
49
+ - A "Source Control" panel powered by `jupyterlab-git` and `@pierre/diffs`.
50
+ - An agent launcher with a prompt textarea, a row of agent buttons (Claude,
51
+ Codex, Gemini, Copilot, Goose, OpenCode, Kiro, Mistral Vibe), and a
52
+ collapsible list of changed files. Buttons are filtered to agents installed
53
+ on the machine; a typed prompt is shell-quoted and spliced into the launch
54
+ command for agents that accept one.
55
+
56
+ ## Language servers
57
+
58
+ xtralab ships with
59
+ [`jupyterlab-lsp`](https://github.com/jupyter-lsp/jupyterlab-lsp) and
60
+ pre-registers two language servers through
61
+ `jupyter_server_config.d/xtralab-lsp.json`:
62
+
63
+ - **Python — [`ty`](https://github.com/astral-sh/ty)** — installed as a
64
+ Python dependency. Works out of the box.
65
+ - **TypeScript / JavaScript — `typescript-language-server`** — install
66
+ yourself:
67
+
68
+ ```bash
69
+ npm install -g typescript-language-server typescript
70
+ ```
71
+
72
+ Restart JupyterLab (or the desktop app) after installing.
73
+
74
+ Specs use bare command names (`["ty", "server"]`), so binaries are resolved
75
+ from `PATH` at spawn time.
76
+
77
+ ### Where binaries are discovered
78
+
79
+ - **`pip install xtralab`** — anything on the JupyterLab process's `PATH`.
80
+ - **Desktop app** — the supervisor augments `PATH` with common shim
81
+ locations (`~/.volta/bin`, `~/.npm-global/bin`, `~/.bun/bin`,
82
+ `~/.asdf/shims`, `~/.mise/shims`, `/opt/homebrew/bin`, `/usr/local/bin`,
83
+ …). Set `XTRALAB_EXTRA_PATH` (colon-separated) before launching the app
84
+ to add directories the defaults miss.
85
+
86
+ ### Adding more servers
87
+
88
+ To enable another server (bash, yaml, json, dockerfile, pyright, …),
89
+ install the binary then drop a JSON file into a `jupyter_server_config.d/`
90
+ directory:
91
+
92
+ - **`pip install xtralab`** — run `jupyter --paths` and pick a config dir
93
+ (typically `~/.jupyter/jupyter_server_config.d/`).
94
+ - **Desktop app (macOS)** —
95
+ `~/Library/Application Support/xtralab/jupyter/config/jupyter_server_config.d/`
96
+ (or `xtralab dev` for local dev builds).
97
+ - **Desktop app (Linux AppImage)** —
98
+ `~/.config/xtralab/jupyter/config/jupyter_server_config.d/`.
99
+
100
+ Example (`bash-lsp.json`):
101
+
102
+ ```json
103
+ {
104
+ "LanguageServerManager": {
105
+ "language_servers": {
106
+ "bash-language-server": {
107
+ "version": 2,
108
+ "argv": ["bash-language-server", "start"],
109
+ "languages": ["bash", "sh"],
110
+ "mime_types": ["text/x-sh", "application/x-sh"],
111
+ "display_name": "bash-language-server"
112
+ }
113
+ }
114
+ }
115
+ }
116
+ ```
42
117
 
43
- - The announcements plugin is disabled, so JupyterLab does not prompt to fetch
44
- news from the Jupyter news feed and does not check for application updates.
45
- - The Table of Contents, Debugger, and Notebook Tools panels are disabled so
46
- they do not show up in the right sidebar by default.
47
- - `dockPanelPadding` is off, so the main dock area is flush with the
48
- surrounding chrome.
118
+ Pair with `npm install -g bash-language-server`. Reuse a bundled `key` to
119
+ override it. See
120
+ [jupyterlab-lsp's docs](https://jupyterlab-lsp.readthedocs.io/en/latest/Configuring.html)
121
+ for the full spec schema.
49
122
 
50
- The JupyterLab frontend defaults are shipped as `labconfig/*.d/00-xtralab.json`
51
- fragments so downstream meta-packages can add their own Lab configuration
52
- fragments without replacing `xtralab`'s files.
123
+ ## Customizing the launcher
53
124
 
54
- ## Install
125
+ Open `Settings → Settings Editor → xtralab launcher` and edit the `agents`
126
+ array. Entries are merged with the defaults by `id`.
55
127
 
56
- ```bash
57
- pip install xtralab
58
- ```
128
+ ```jsonc
129
+ {
130
+ "agents": [
131
+ // Hide an agent
132
+ { "id": "kiro", "enabled": false },
59
133
 
60
- ## Development
134
+ // Override an agent's command (e.g. point Claude at a shell alias)
135
+ { "id": "claude", "command": "cl", "requireAvailable": false },
61
136
 
62
- ```bash
63
- # Install dependencies and the package in editable mode
64
- uv pip install -e ".[dev]"
65
- jlpm
66
- jlpm build
137
+ // Add a new agent; promptArgs: [] appends the prompt as a positional arg
138
+ { "id": "aider", "label": "Aider", "command": "aider", "promptArgs": [] }
139
+ ]
140
+ }
67
141
  ```
68
142
 
143
+ Fields: `id` (required), `label`, `caption`, `command`, `promptArgs` (how to
144
+ splice the prompt — `[]` for positional, `["--flag"]` for flagged, `null` to
145
+ opt out), `iconSvg`, `rank`, `enabled`, `requireAvailable`.
146
+
147
+ ## Contributing
148
+
149
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the development setup, the
150
+ Electron desktop app architecture, and the build pipeline.
151
+
69
152
  ## License
70
153
 
71
154
  BSD-3-Clause
@@ -1,7 +1,7 @@
1
1
  import { JupyterFrontEnd } from '@jupyterlab/application';
2
2
  import { IXtralabFileBrowser } from './widget';
3
3
  /**
4
- * Command identifiers exposed by the Xtralab browser. We deliberately namespace
4
+ * Command identifiers exposed by the xtralab browser. We deliberately namespace
5
5
  * these under `xtralab:` rather than reusing the `filebrowser:` ids
6
6
  * because the core `filebrowser:*` commands look up a `FileBrowser` instance
7
7
  * via `IFileBrowserFactory.tracker` — our widget is not a `FileBrowser`, so
@@ -31,7 +31,7 @@ export interface IRegisterCommandsOptions {
31
31
  browser: IXtralabFileBrowser;
32
32
  }
33
33
  /**
34
- * Register every Xtralab command on the application command registry, attach
34
+ * Register every xtralab command on the application command registry, attach
35
35
  * the items to the application context menu, and wire up the dynamic
36
36
  * "Open With" submenu populator.
37
37
  *
@@ -3,7 +3,7 @@ import { PageConfig, PathExt } from '@jupyterlab/coreutils';
3
3
  import { addIcon, closeIcon, CommandToolbarButton, copyIcon, downloadIcon, editIcon, fileIcon, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
4
4
  import { toCanonicalPath, toServerPath } from './contents';
5
5
  /**
6
- * Command identifiers exposed by the Xtralab browser. We deliberately namespace
6
+ * Command identifiers exposed by the xtralab browser. We deliberately namespace
7
7
  * these under `xtralab:` rather than reusing the `filebrowser:` ids
8
8
  * because the core `filebrowser:*` commands look up a `FileBrowser` instance
9
9
  * via `IFileBrowserFactory.tracker` — our widget is not a `FileBrowser`, so
@@ -136,7 +136,7 @@ function makeOpenWithUpdater(app, browser) {
136
136
  };
137
137
  }
138
138
  /**
139
- * Register every Xtralab command on the application command registry, attach
139
+ * Register every xtralab command on the application command registry, attach
140
140
  * the items to the application context menu, and wire up the dynamic
141
141
  * "Open With" submenu populator.
142
142
  *
@@ -4,6 +4,7 @@ import { MimeData, PromiseDelegate } from '@lumino/coreutils';
4
4
  import { Drag } from '@lumino/dragdrop';
5
5
  import { FileTree, useFileTree } from '@pierre/trees/react';
6
6
  import { ROOT_LOAD_KEY, listDirectory, toServerPath } from './contents';
7
+ import { buildIgnoredEntries, loadGitignoreMatcher } from './gitignore';
7
8
  import { FILE_BROWSER_ICONS } from './icons';
8
9
  /**
9
10
  * MIME types used by the dock panel and other JupyterLab drop targets.
@@ -11,7 +12,7 @@ import { FILE_BROWSER_ICONS } from './icons';
11
12
  * synchronous function that returns a Widget. The contents MIME types
12
13
  * mirror what the default file browser sends so other drop targets that
13
14
  * understand them (the file browser itself, custom drop zones) keep
14
- * working when the user drags out of Xtralab.
15
+ * working when the user drags out of xtralab.
15
16
  */
16
17
  const FACTORY_MIME = 'application/vnd.lumino.widget-factory';
17
18
  const CONTENTS_MIME = 'application/x-jupyter-icontents';
@@ -41,11 +42,54 @@ export function FileBrowserComponent(props) {
41
42
  paths: [],
42
43
  initialExpansion: 'closed',
43
44
  search: true,
44
- icons: FILE_BROWSER_ICONS
45
+ icons: FILE_BROWSER_ICONS,
46
+ itemHeight: 24
45
47
  });
46
48
  React.useEffect(() => {
47
49
  const knownDirs = new Map();
50
+ // Mirror of the canonical paths currently loaded into the tree. Kept in
51
+ // sync with `model.resetPaths`/`model.batch`/`model.add` so we can
52
+ // re-test every loaded path against the gitignore matcher whenever
53
+ // either side changes — the model itself does not expose a
54
+ // path-iteration API.
55
+ const loadedPaths = new Set();
56
+ let gitignoreMatcher = null;
48
57
  let cancelled = false;
58
+ /**
59
+ * Recompute the `ignored` git status entries from the current
60
+ * gitignore matcher and the set of loaded paths, and push them into
61
+ * the tree. Safe to call at any time: when the matcher is `null`
62
+ * (no `.gitignore` or it failed to load) we send an empty list, which
63
+ * also clears any statuses that may have been applied previously.
64
+ */
65
+ const syncGitStatus = () => {
66
+ if (gitignoreMatcher === null) {
67
+ model.setGitStatus([]);
68
+ return;
69
+ }
70
+ const entries = buildIgnoredEntries(gitignoreMatcher, loadedPaths);
71
+ model.setGitStatus(entries);
72
+ };
73
+ /**
74
+ * Reload the workspace `.gitignore`, then re-apply the resulting
75
+ * ignored statuses. Called on initial mount and whenever the user
76
+ * triggers a refresh — the file may have been edited or created in
77
+ * between.
78
+ */
79
+ const refreshGitignoreMatcher = async () => {
80
+ let next = null;
81
+ try {
82
+ next = await loadGitignoreMatcher(contentsManager);
83
+ }
84
+ catch (err) {
85
+ console.error('xtralab: failed to load .gitignore', err);
86
+ }
87
+ if (cancelled) {
88
+ return;
89
+ }
90
+ gitignoreMatcher = next;
91
+ syncGitStatus();
92
+ };
49
93
  const fetchDirectory = async (canonicalPath) => {
50
94
  const current = knownDirs.get(canonicalPath);
51
95
  if (current === 'loading' || current === 'loaded') {
@@ -60,6 +104,10 @@ export function FileBrowserComponent(props) {
60
104
  }
61
105
  if (canonicalPath === ROOT_LOAD_KEY) {
62
106
  model.resetPaths(paths);
107
+ loadedPaths.clear();
108
+ for (const path of paths) {
109
+ loadedPaths.add(path);
110
+ }
63
111
  }
64
112
  else {
65
113
  // Filter out paths already in the model. The path-store throws
@@ -72,6 +120,14 @@ export function FileBrowserComponent(props) {
72
120
  if (operations.length > 0) {
73
121
  model.batch(operations);
74
122
  }
123
+ // Mirror every directory child into `loadedPaths` regardless of
124
+ // whether it was just added or skipped above — paths skipped by
125
+ // the filter are already in the model from a prior add and so
126
+ // belong in the mirror too. `Set.add` is idempotent, so the
127
+ // duplicate adds are a no-op.
128
+ for (const path of paths) {
129
+ loadedPaths.add(path);
130
+ }
75
131
  }
76
132
  for (const subdir of subdirectories) {
77
133
  if (!knownDirs.has(subdir)) {
@@ -79,6 +135,7 @@ export function FileBrowserComponent(props) {
79
135
  }
80
136
  }
81
137
  knownDirs.set(canonicalPath, 'loaded');
138
+ syncGitStatus();
82
139
  }
83
140
  catch (err) {
84
141
  console.error(`xtralab: failed to load directory "${canonicalPath}"`, err);
@@ -165,12 +222,22 @@ export function FileBrowserComponent(props) {
165
222
  // Reset the tree contents and restore the load-state map so future
166
223
  // expansions know which directories still need fetching.
167
224
  model.resetPaths(allPaths);
225
+ loadedPaths.clear();
226
+ for (const path of allPaths) {
227
+ loadedPaths.add(path);
228
+ }
168
229
  knownDirs.set(ROOT_LOAD_KEY, 'loaded');
169
230
  subdirsByParent.forEach(subdirs => {
170
231
  for (const subdir of subdirs) {
171
232
  knownDirs.set(subdir, expandedPaths.has(subdir) ? 'loaded' : 'unloaded');
172
233
  }
173
234
  });
235
+ // Refresh the `.gitignore` matcher in case the file was edited
236
+ // since the last load, then re-apply the resulting statuses to the
237
+ // newly-loaded paths. The reload runs in parallel with the rest of
238
+ // the refresh — `refreshGitignoreMatcher` calls `syncGitStatus`
239
+ // itself when it completes.
240
+ void refreshGitignoreMatcher();
174
241
  // Re-expand the directories that were expanded before the refresh.
175
242
  // We have to do this after `resetPaths` because the reset starts
176
243
  // every directory in its initial collapsed state.
@@ -196,12 +263,14 @@ export function FileBrowserComponent(props) {
196
263
  return;
197
264
  }
198
265
  }
266
+ loadedPaths.add(canonicalPath);
199
267
  if (canonicalPath.endsWith('/') && !knownDirs.has(canonicalPath)) {
200
268
  // The new directory has no children yet, so mark it as already
201
269
  // loaded — there's nothing to fetch and we don't want a stale
202
270
  // "unloaded" entry to trigger a fetch on the next expand.
203
271
  knownDirs.set(canonicalPath, 'loaded');
204
272
  }
273
+ syncGitStatus();
205
274
  const parent = parentOf(canonicalPath);
206
275
  if (parent === ROOT_LOAD_KEY) {
207
276
  return;
@@ -217,6 +286,7 @@ export function FileBrowserComponent(props) {
217
286
  };
218
287
  knownDirs.set(ROOT_LOAD_KEY, 'unloaded');
219
288
  void fetchDirectory(ROOT_LOAD_KEY);
289
+ void refreshGitignoreMatcher();
220
290
  const unsubscribe = model.subscribe(() => {
221
291
  knownDirs.forEach((state, canonicalPath) => {
222
292
  if (state !== 'unloaded') {
@@ -0,0 +1,29 @@
1
+ import type { Ignore } from 'ignore';
2
+ import { Contents } from '@jupyterlab/services';
3
+ import type { GitStatusEntry } from '@pierre/trees';
4
+ /**
5
+ * Load the workspace `.gitignore` and return an `ignore` matcher built from
6
+ * its contents. Returns `null` when the file is missing, empty, or fails to
7
+ * load — callers should treat any of those as "no ignored entries".
8
+ */
9
+ export declare function loadGitignoreMatcher(contentsManager: Contents.IManager): Promise<Ignore | null>;
10
+ /**
11
+ * Build `GitStatusEntry`s for the canonical paths that match the given
12
+ * gitignore matcher.
13
+ *
14
+ * `@pierre/trees` propagates the `ignored` style from a directory entry
15
+ * down to every descendant via its internal `ignoredDirectoryPaths` set, so
16
+ * we only emit one entry per ignored subtree root: emitting descendants
17
+ * would also pollute the library's `directoriesWithChanges` set, causing
18
+ * parent directories of nested ignored items to render a "contains git
19
+ * status items" dot.
20
+ *
21
+ * Paths are sorted lexicographically so a directory always precedes its
22
+ * descendants, allowing a simple ancestor-suppression check via
23
+ * `path.startsWith(dir)`. The trailing slash on canonical directory paths
24
+ * (required by `@pierre/trees`, also recognized by `ignore` as the
25
+ * "directory" indicator) keeps the prefix check from accidentally matching
26
+ * sibling paths that happen to share a name prefix (e.g. `node_modules_old`
27
+ * vs `node_modules/`).
28
+ */
29
+ export declare function buildIgnoredEntries(matcher: Ignore, canonicalPaths: Iterable<string>): GitStatusEntry[];
@@ -0,0 +1,74 @@
1
+ import ignore from 'ignore';
2
+ /**
3
+ * The path of the workspace `.gitignore` file. Loaded relative to the
4
+ * Jupyter contents root, which matches the directory the gitignore patterns
5
+ * are evaluated against — patterns and tested paths share the same base.
6
+ */
7
+ const GITIGNORE_PATH = '.gitignore';
8
+ /**
9
+ * Load the workspace `.gitignore` and return an `ignore` matcher built from
10
+ * its contents. Returns `null` when the file is missing, empty, or fails to
11
+ * load — callers should treat any of those as "no ignored entries".
12
+ */
13
+ export async function loadGitignoreMatcher(contentsManager) {
14
+ let model;
15
+ try {
16
+ model = await contentsManager.get(GITIGNORE_PATH, {
17
+ content: true,
18
+ format: 'text',
19
+ type: 'file'
20
+ });
21
+ }
22
+ catch (_a) {
23
+ // The most common failure here is `.gitignore` simply not existing.
24
+ // Other transient errors (permission denied, server unreachable) are
25
+ // also treated as "no matcher" so the file browser still works.
26
+ return null;
27
+ }
28
+ if (model.type !== 'file' || typeof model.content !== 'string') {
29
+ return null;
30
+ }
31
+ if (model.content.length === 0) {
32
+ return null;
33
+ }
34
+ return ignore().add(model.content);
35
+ }
36
+ /**
37
+ * Build `GitStatusEntry`s for the canonical paths that match the given
38
+ * gitignore matcher.
39
+ *
40
+ * `@pierre/trees` propagates the `ignored` style from a directory entry
41
+ * down to every descendant via its internal `ignoredDirectoryPaths` set, so
42
+ * we only emit one entry per ignored subtree root: emitting descendants
43
+ * would also pollute the library's `directoriesWithChanges` set, causing
44
+ * parent directories of nested ignored items to render a "contains git
45
+ * status items" dot.
46
+ *
47
+ * Paths are sorted lexicographically so a directory always precedes its
48
+ * descendants, allowing a simple ancestor-suppression check via
49
+ * `path.startsWith(dir)`. The trailing slash on canonical directory paths
50
+ * (required by `@pierre/trees`, also recognized by `ignore` as the
51
+ * "directory" indicator) keeps the prefix check from accidentally matching
52
+ * sibling paths that happen to share a name prefix (e.g. `node_modules_old`
53
+ * vs `node_modules/`).
54
+ */
55
+ export function buildIgnoredEntries(matcher, canonicalPaths) {
56
+ const sorted = Array.from(canonicalPaths).sort();
57
+ const emittedIgnoredDirs = [];
58
+ const entries = [];
59
+ for (const path of sorted) {
60
+ if (path.length === 0) {
61
+ continue;
62
+ }
63
+ if (emittedIgnoredDirs.some(dir => path.startsWith(dir))) {
64
+ continue;
65
+ }
66
+ if (matcher.ignores(path)) {
67
+ entries.push({ path, status: 'ignored' });
68
+ if (path.endsWith('/')) {
69
+ emittedIgnoredDirs.push(path);
70
+ }
71
+ }
72
+ }
73
+ return entries;
74
+ }
@@ -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
  */
@@ -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`).
@@ -32,7 +32,7 @@ export class XtralabFileBrowser extends Widget {
32
32
  this._onOpenFile = options.onOpenFile;
33
33
  this.id = FILE_BROWSER_ID;
34
34
  this.title.icon = xtralabFileBrowserIcon;
35
- this.title.caption = 'Xtralab File Browser';
35
+ this.title.caption = 'xtralab File Browser';
36
36
  this.addClass(FILE_BROWSER_CSS_CLASS);
37
37
  this._toolbar = new Toolbar();
38
38
  this._toolbar.addClass(TOOLBAR_CSS_CLASS);
package/lib/git/api.d.ts CHANGED
@@ -28,6 +28,17 @@ export declare function reset(repoPath: string, filename: string): Promise<void>
28
28
  export declare function resetAll(repoPath: string): Promise<void>;
29
29
  /** Restore a single file to the version in the index (i.e. discard worktree changes). */
30
30
  export declare function checkout(repoPath: string, filename: string): Promise<void>;
31
+ /**
32
+ * Expand the porcelain `files` array into one entry per logical change.
33
+ * A single porcelain row may map to two `IFileChange` rows (a file that's
34
+ * both staged and modified again in the worktree); callers that just want
35
+ * a flat list of all changes — without any staged-vs-unstaged grouping —
36
+ * can use this helper.
37
+ *
38
+ * Provided as a manual concat rather than `Array.prototype.flatMap` so we
39
+ * don't pin our `tsconfig.lib` to ES2019.
40
+ */
41
+ export declare function expandStatusFiles(files: IGitStatusFile[]): IFileChange[];
31
42
  /**
32
43
  * Translate one entry in the porcelain `files` array into the panel's
33
44
  * preferred shape. A single porcelain entry may represent up to two changes
package/lib/git/api.js CHANGED
@@ -103,6 +103,25 @@ export async function checkout(repoPath, filename) {
103
103
  filename
104
104
  });
105
105
  }
106
+ /**
107
+ * Expand the porcelain `files` array into one entry per logical change.
108
+ * A single porcelain row may map to two `IFileChange` rows (a file that's
109
+ * both staged and modified again in the worktree); callers that just want
110
+ * a flat list of all changes — without any staged-vs-unstaged grouping —
111
+ * can use this helper.
112
+ *
113
+ * Provided as a manual concat rather than `Array.prototype.flatMap` so we
114
+ * don't pin our `tsconfig.lib` to ES2019.
115
+ */
116
+ export function expandStatusFiles(files) {
117
+ const result = [];
118
+ for (const file of files) {
119
+ for (const change of porcelainToFileChanges(file)) {
120
+ result.push(change);
121
+ }
122
+ }
123
+ return result;
124
+ }
106
125
  /**
107
126
  * Translate one entry in the porcelain `files` array into the panel's
108
127
  * preferred shape. A single porcelain entry may represent up to two changes
@@ -24,6 +24,7 @@ export declare namespace CommandArguments {
24
24
  interface IOpenDiff {
25
25
  repoPath: string;
26
26
  change: IFileChange;
27
+ pin?: boolean;
27
28
  }
28
29
  }
29
30
  export interface IRegisterGitCommandsOptions {
@@ -58,7 +59,8 @@ export interface IRegisterGitCommandsOptions {
58
59
  * Look up an already-open diff widget for a given file change. Returns
59
60
  * `undefined` when none is open.
60
61
  */
61
- findDiff(change: IFileChange): ReturnType<typeof createGitDiffWidget> | undefined;
62
+ findDiff(change: IFileChange, pin?: boolean): ReturnType<typeof createGitDiffWidget> | undefined;
63
+ onPinned(widget: ReturnType<typeof createGitDiffWidget>): void;
62
64
  }
63
65
  /**
64
66
  * Register the git commands on the application command registry. Returns