xtralab 0.1.1 → 0.2.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 (65) hide show
  1. package/README.md +86 -0
  2. package/lib/fileBrowser/fileBrowser.js +71 -1
  3. package/lib/fileBrowser/gitignore.d.ts +29 -0
  4. package/lib/fileBrowser/gitignore.js +74 -0
  5. package/lib/fileBrowser/index.js +1 -1
  6. package/lib/git/api.d.ts +11 -0
  7. package/lib/git/api.js +19 -0
  8. package/lib/git/commands.d.ts +3 -1
  9. package/lib/git/commands.js +10 -4
  10. package/lib/git/diffWidget.d.ts +11 -0
  11. package/lib/git/diffWidget.js +90 -9
  12. package/lib/git/index.js +23 -7
  13. package/lib/git/panelComponent.d.ts +3 -1
  14. package/lib/git/panelComponent.js +6 -3
  15. package/lib/index.js +9 -1
  16. package/lib/launcher/agents.d.ts +78 -0
  17. package/lib/launcher/agents.js +168 -0
  18. package/lib/launcher/availability.d.ts +12 -0
  19. package/lib/launcher/availability.js +53 -0
  20. package/lib/launcher/commands.d.ts +22 -0
  21. package/lib/launcher/commands.js +89 -0
  22. package/lib/launcher/dashboard.d.ts +46 -0
  23. package/lib/launcher/dashboard.js +164 -0
  24. package/lib/launcher/icons.d.ts +33 -0
  25. package/lib/launcher/icons.js +113 -0
  26. package/lib/launcher/index.d.ts +23 -0
  27. package/lib/launcher/index.js +191 -0
  28. package/lib/launcher/invocation.d.ts +29 -0
  29. package/lib/launcher/invocation.js +42 -0
  30. package/lib/menus/index.d.ts +14 -0
  31. package/lib/menus/index.js +102 -0
  32. package/lib/sidebar/index.d.ts +18 -0
  33. package/lib/sidebar/index.js +155 -0
  34. package/lib/statusBar/index.d.ts +14 -0
  35. package/lib/statusBar/index.js +89 -0
  36. package/lib/statusBar/widget.d.ts +121 -0
  37. package/lib/statusBar/widget.js +237 -0
  38. package/package.json +10 -2
  39. package/schema/launcher.json +70 -0
  40. package/schema/sidebar.json +57 -0
  41. package/src/fileBrowser/fileBrowser.tsx +75 -1
  42. package/src/fileBrowser/gitignore.ts +86 -0
  43. package/src/fileBrowser/index.ts +1 -1
  44. package/src/git/api.ts +20 -0
  45. package/src/git/commands.ts +19 -5
  46. package/src/git/diffWidget.tsx +109 -13
  47. package/src/git/index.ts +32 -6
  48. package/src/git/panelComponent.tsx +17 -5
  49. package/src/index.ts +9 -1
  50. package/src/launcher/agents.ts +248 -0
  51. package/src/launcher/availability.ts +73 -0
  52. package/src/launcher/commands.ts +103 -0
  53. package/src/launcher/dashboard.tsx +356 -0
  54. package/src/launcher/icons.ts +123 -0
  55. package/src/launcher/index.ts +228 -0
  56. package/src/launcher/invocation.ts +45 -0
  57. package/src/menus/index.ts +124 -0
  58. package/src/sidebar/index.ts +195 -0
  59. package/src/statusBar/index.ts +115 -0
  60. package/src/statusBar/widget.tsx +318 -0
  61. package/style/git.css +4 -8
  62. package/style/index.css +2 -0
  63. package/style/index.js +2 -0
  64. package/style/launcher.css +323 -0
  65. package/style/statusBar.css +95 -0
package/README.md CHANGED
@@ -35,6 +35,61 @@ override versions and Jupyter configuration in the usual ways.
35
35
  `jupyterlab-git` server REST API and `@pierre/diffs`. The bundled
36
36
  `@jupyterlab/git` frontend is disabled automatically so the two panels do
37
37
  not coexist.
38
+ - An agent-focused launcher that replaces the stock JupyterLab launcher.
39
+ The launcher tab shows an optional initial-prompt textarea, a row of
40
+ agent buttons (Claude, Codex, Gemini, Copilot, Goose, OpenCode, Kiro,
41
+ and Mistral Vibe), and a collapsible list of changed files (each
42
+ clickable into the diff viewer) below them. Clicking an agent opens a
43
+ fresh terminal and runs the agent's CLI; if the prompt textarea is
44
+ non-empty, the prompt is shell-quoted and spliced into the launch line
45
+ for agents that accept one. Agents are filtered by a `which`-based
46
+ availability check at activation, so only the ones actually installed
47
+ on the machine show up.
48
+
49
+ ### Customizing the launcher
50
+
51
+ The launcher reads `xtralab:launcher` settings from the JupyterLab settings
52
+ registry. Open the Settings Editor (`Settings → Settings Editor → xtralab
53
+ launcher`) and edit the `agents` array; xtralab merges your entries with
54
+ the defaults by `id`.
55
+
56
+ ```jsonc
57
+ {
58
+ "agents": [
59
+ // Hide an agent you never want to see, even if it's installed:
60
+ { "id": "kiro", "enabled": false },
61
+
62
+ // Override an agent's command — e.g. point Claude at a shell alias.
63
+ // `requireAvailable: false` keeps the card visible even though
64
+ // `which cl` returns nothing (interactive shells expand the alias).
65
+ { "id": "claude", "command": "cl", "requireAvailable": false },
66
+
67
+ // Add a brand-new agent. `iconSvg` is optional; without it the card
68
+ // falls back to a generic terminal icon. `promptArgs: []` means the
69
+ // launcher's prompt textarea is appended as a positional argument.
70
+ {
71
+ "id": "aider",
72
+ "label": "Aider",
73
+ "command": "aider",
74
+ "promptArgs": []
75
+ }
76
+ ]
77
+ }
78
+ ```
79
+
80
+ Field reference (every field except `id` is optional):
81
+
82
+ | Field | Effect |
83
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `id` | Built-in id to override, or a new id to define a new card. |
85
+ | `label` | Card title. |
86
+ | `caption` | Subtitle shown beneath the title. |
87
+ | `command` | Literal text typed into the new terminal. |
88
+ | `iconSvg` | Inline SVG for the card icon (only needed for new agents). |
89
+ | `rank` | Sort order on the launcher; lower comes first. |
90
+ | `enabled` | `false` hides the card from the launcher and palette. |
91
+ | `requireAvailable` | `false` skips the `which` check (use for shell aliases that aren't on PATH but expand at runtime). |
92
+ | `promptArgs` | How to splice the launcher's optional prompt into the command. `[]` appends it as a positional argument (`<command> 'PROMPT'`); `["-i"]` or `["--prompt"]` puts it after a flag. Set to `null` to opt out — the agent button is dimmed when the prompt textarea is non-empty so a typed prompt is never silently dropped. Defaults vary per agent. |
38
93
 
39
94
  ## Default settings
40
95
 
@@ -44,6 +99,12 @@ override versions and Jupyter configuration in the usual ways.
44
99
  news from the Jupyter news feed and does not check for application updates.
45
100
  - The Table of Contents, Debugger, and Notebook Tools panels are disabled so
46
101
  they do not show up in the right sidebar by default.
102
+ - `dockPanelPadding` is off, so the main dock area is flush with the
103
+ surrounding chrome.
104
+ - The terminal `fontFamily` defaults to `MesloLGS NF, ui-monospace, monospace`
105
+ so Powerline / Nerd Font glyphs (used by Oh My Zsh themes such as
106
+ `powerlevel10k`) render correctly when the font is installed; the
107
+ `ui-monospace, monospace` fallback keeps things readable when it is not.
47
108
 
48
109
  The JupyterLab frontend defaults are shipped as `labconfig/*.d/00-xtralab.json`
49
110
  fragments so downstream meta-packages can add their own Lab configuration
@@ -55,6 +116,31 @@ fragments without replacing `xtralab`'s files.
55
116
  pip install xtralab
56
117
  ```
57
118
 
119
+ ## Run as a desktop app
120
+
121
+ `xtralab` ships a tiny launcher that opens JupyterLab in a dedicated Chrome
122
+ window (via Chrome's `--app` mode), with an isolated user-data directory so it
123
+ does not share cookies, extensions, or history with your normal browser.
124
+
125
+ Requires Chrome or Chromium installed. macOS only for now; other platforms will be added later.
126
+
127
+ The simplest way, no install needed:
128
+
129
+ ```bash
130
+ uvx xtralab
131
+ ```
132
+
133
+ Or, after `pip install xtralab` into an environment:
134
+
135
+ ```bash
136
+ xtralab
137
+ ```
138
+
139
+ The launcher starts a local Jupyter server on a random port with a fresh
140
+ token, waits for it to come up, opens the app window, and shuts the server
141
+ down when you close the window. `Ctrl-C` in the launching terminal also
142
+ shuts everything down cleanly.
143
+
58
144
  ## Development
59
145
 
60
146
  ```bash
@@ -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.
@@ -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
+ }
@@ -22,7 +22,7 @@ const plugin = {
22
22
  void app.commands.execute('docmanager:open', { path: serverPath });
23
23
  }
24
24
  });
25
- app.shell.add(browser, 'left', { rank: 700 });
25
+ app.shell.add(browser, 'left', { rank: 50 });
26
26
  if (restorer) {
27
27
  restorer.add(browser, browser.id);
28
28
  }
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
@@ -1,5 +1,5 @@
1
1
  import { refreshIcon } from '@jupyterlab/ui-components';
2
- import { createGitDiffWidget, diffWidgetId } from './diffWidget';
2
+ import { createGitDiffWidget, diffWidgetId, updateGitDiffWidget } from './diffWidget';
3
3
  /**
4
4
  * Command IDs exposed by the git plugin. Both are registered in
5
5
  * {@link registerGitCommands} so other extensions, the command palette and
@@ -16,7 +16,7 @@ export var CommandIDs;
16
16
  * (the commands themselves stay registered for the lifetime of the plugin).
17
17
  */
18
18
  export function registerGitCommands(options) {
19
- const { app, panel, themeManager, commandPalette, contentsManager, rendermime, onChanged, trackDiff, findDiff } = options;
19
+ const { app, panel, themeManager, commandPalette, contentsManager, rendermime, onChanged, trackDiff, findDiff, onPinned } = options;
20
20
  const { commands } = app;
21
21
  commands.addCommand(CommandIDs.refresh, {
22
22
  label: 'Refresh Git Changes',
@@ -34,8 +34,12 @@ export function registerGitCommands(options) {
34
34
  if ((typed === null || typed === void 0 ? void 0 : typed.change) === undefined) {
35
35
  return;
36
36
  }
37
- const existing = findDiff(typed.change);
37
+ const pin = typed.pin === true;
38
+ const existing = findDiff(typed.change, pin);
38
39
  if (existing !== undefined && !existing.isDisposed) {
40
+ if (!pin) {
41
+ updateGitDiffWidget(existing, typed.change);
42
+ }
39
43
  app.shell.activateById(existing.id);
40
44
  return;
41
45
  }
@@ -45,7 +49,9 @@ export function registerGitCommands(options) {
45
49
  themeManager,
46
50
  contentsManager,
47
51
  rendermime,
48
- onChanged
52
+ onChanged,
53
+ pinned: pin,
54
+ onPinned
49
55
  });
50
56
  await trackDiff(widget);
51
57
  app.shell.add(widget, 'main', { mode: 'tab-after' });
@@ -10,6 +10,8 @@ import { GitReference, IFileChange } from './tokens';
10
10
  * style the embedded `@pierre/diffs` viewer hang off this class.
11
11
  */
12
12
  export declare const DIFF_WIDGET_CSS_CLASS = "jp-xtralab-DiffWidget";
13
+ export declare const PREVIEW_DIFF_WIDGET_ID = "xtralab:diff:preview";
14
+ export declare function pinnedDiffWidgetId(change: IFileChange): string;
13
15
  /**
14
16
  * View modes available for `.ipynb` diffs. `notebook` is the cell-by-cell
15
17
  * rendered view; `json` falls back to the raw nbformat JSON file diff so
@@ -58,6 +60,8 @@ export interface IGitDiffWidgetOptions {
58
60
  * would just be visual noise.
59
61
  */
60
62
  onEmpty?: () => void;
63
+ onPinned?: (widget: MainAreaWidget<DiffContentWidget>) => void;
64
+ pinned?: boolean;
61
65
  }
62
66
  /**
63
67
  * Build a `MainAreaWidget`-hosted diff viewer for a single file change.
@@ -67,6 +71,7 @@ export interface IGitDiffWidgetOptions {
67
71
  * read-only main-area widgets in the JupyterLab ecosystem.
68
72
  */
69
73
  export declare function createGitDiffWidget(options: IGitDiffWidgetOptions): MainAreaWidget<DiffContentWidget>;
74
+ export declare function updateGitDiffWidget(widget: MainAreaWidget<DiffContentWidget>, change: IFileChange): void;
70
75
  /**
71
76
  * The actual React-rendered region of the diff viewer. Held in its own
72
77
  * `ReactWidget` so the surrounding `MainAreaWidget` can manage chrome
@@ -98,11 +103,17 @@ export declare class DiffContentWidget extends ReactWidget {
98
103
  setHasNotebookView(value: boolean): void;
99
104
  /** Emits whenever {@link hasNotebookView} changes. */
100
105
  get hasNotebookViewChanged(): ISignal<this, boolean>;
106
+ get pinned(): boolean;
107
+ pin(): void;
108
+ get pinnedChanged(): ISignal<this, boolean>;
109
+ setChange(change: IFileChange): void;
101
110
  protected render(): React.ReactElement;
102
111
  private _options;
103
112
  private _notebookViewMode;
104
113
  private _notebookViewModeChanged;
105
114
  private _hasNotebookView;
106
115
  private _hasNotebookViewChanged;
116
+ private _pinned;
117
+ private _pinnedChanged;
107
118
  }
108
119
  export {};