xtralab 0.4.0 → 0.6.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 (96) hide show
  1. package/README.md +110 -73
  2. package/lib/agentSessions.d.ts +50 -0
  3. package/lib/agentSessions.js +45 -0
  4. package/lib/fileBrowser/index.js +5 -1
  5. package/lib/git/commands.d.ts +12 -13
  6. package/lib/git/commands.js +7 -28
  7. package/lib/git/diffProvider.d.ts +8 -0
  8. package/lib/git/diffProvider.js +314 -0
  9. package/lib/git/diffSurface.d.ts +115 -0
  10. package/lib/git/diffSurface.js +342 -0
  11. package/lib/git/diffWidget.d.ts +8 -13
  12. package/lib/git/diffWidget.js +65 -311
  13. package/lib/git/imageDiff.d.ts +23 -0
  14. package/lib/git/imageDiff.js +256 -0
  15. package/lib/git/index.d.ts +10 -8
  16. package/lib/git/index.js +37 -52
  17. package/lib/index.d.ts +4 -2
  18. package/lib/index.js +14 -8
  19. package/lib/launcher/agents.d.ts +1 -1
  20. package/lib/launcher/agents.js +22 -13
  21. package/lib/launcher/commands.d.ts +32 -6
  22. package/lib/launcher/commands.js +81 -57
  23. package/lib/launcher/dashboard.d.ts +15 -0
  24. package/lib/launcher/dashboard.js +119 -5
  25. package/lib/launcher/editorRegistry.d.ts +43 -0
  26. package/lib/launcher/editorRegistry.js +96 -0
  27. package/lib/launcher/editors.d.ts +78 -0
  28. package/lib/launcher/editors.js +110 -0
  29. package/lib/launcher/icons.d.ts +25 -6
  30. package/lib/launcher/icons.js +116 -19
  31. package/lib/launcher/index.d.ts +2 -22
  32. package/lib/launcher/index.js +48 -25
  33. package/lib/launcher/registry.d.ts +21 -0
  34. package/lib/launcher/registry.js +28 -0
  35. package/lib/launcher/tokens.d.ts +41 -0
  36. package/lib/launcher/tokens.js +17 -0
  37. package/lib/terminals/detection.d.ts +11 -0
  38. package/lib/terminals/detection.js +50 -0
  39. package/lib/terminals/index.d.ts +27 -0
  40. package/lib/terminals/index.js +194 -0
  41. package/lib/terminals/model.d.ts +191 -0
  42. package/lib/terminals/model.js +418 -0
  43. package/lib/terminals/widget.d.ts +72 -0
  44. package/lib/terminals/widget.js +93 -0
  45. package/lib/topBar/icons.d.ts +13 -0
  46. package/lib/topBar/icons.js +27 -0
  47. package/lib/topBar/index.d.ts +16 -0
  48. package/lib/topBar/index.js +86 -0
  49. package/package.json +52 -35
  50. package/schema/launcher.json +56 -1
  51. package/src/agentSessions.ts +94 -0
  52. package/src/fileBrowser/index.ts +5 -1
  53. package/src/git/commands.ts +12 -38
  54. package/src/git/diffProvider.tsx +462 -0
  55. package/src/git/diffSurface.tsx +588 -0
  56. package/src/git/diffWidget.tsx +105 -465
  57. package/src/git/imageDiff.tsx +469 -0
  58. package/src/git/index.ts +38 -68
  59. package/src/index.ts +14 -8
  60. package/src/launcher/agents.ts +23 -14
  61. package/src/launcher/commands.ts +98 -60
  62. package/src/launcher/dashboard.tsx +205 -3
  63. package/src/launcher/editorRegistry.ts +149 -0
  64. package/src/launcher/editors.ts +183 -0
  65. package/src/launcher/icons.ts +120 -20
  66. package/src/launcher/index.ts +62 -27
  67. package/src/launcher/registry.ts +33 -0
  68. package/src/launcher/tokens.ts +51 -0
  69. package/src/terminals/detection.ts +65 -0
  70. package/src/terminals/index.ts +222 -0
  71. package/src/terminals/model.ts +497 -0
  72. package/src/terminals/widget.tsx +240 -0
  73. package/src/topBar/icons.ts +30 -0
  74. package/src/topBar/index.ts +121 -0
  75. package/style/git.css +267 -266
  76. package/style/index.css +2 -1
  77. package/style/index.js +2 -1
  78. package/style/launcher.css +124 -2
  79. package/style/terminals.css +235 -0
  80. package/style/topBar.css +38 -0
  81. package/lib/git/icons.d.ts +0 -14
  82. package/lib/git/icons.js +0 -19
  83. package/lib/git/panel.d.ts +0 -29
  84. package/lib/git/panel.js +0 -42
  85. package/lib/git/panelComponent.d.ts +0 -28
  86. package/lib/git/panelComponent.js +0 -320
  87. package/lib/statusBar/index.d.ts +0 -14
  88. package/lib/statusBar/index.js +0 -89
  89. package/lib/statusBar/widget.d.ts +0 -121
  90. package/lib/statusBar/widget.js +0 -237
  91. package/src/git/icons.ts +0 -20
  92. package/src/git/panel.tsx +0 -56
  93. package/src/git/panelComponent.tsx +0 -575
  94. package/src/statusBar/index.ts +0 -115
  95. package/src/statusBar/widget.tsx +0 -318
  96. package/style/statusBar.css +0 -95
@@ -0,0 +1,256 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * xtralab's image diff, registered in place of `@jupyterlab/git:image-diff`.
4
+ *
5
+ * The git server base64-encodes binary content, so both diff hosts receive
6
+ * the two sides as base64 strings through the same `content()` getters used
7
+ * for text. This module turns those strings into `<img>` elements and
8
+ * offers the 2-up / swipe / onion-skin comparison modes; no diffing library
9
+ * is involved (`@pierre/diffs` is text-only).
10
+ */
11
+ /**
12
+ * Raster image extensions xtralab renders as an image diff, mapped to the
13
+ * MIME subtype used in the `data:image/<subtype>;base64,…` URL.
14
+ *
15
+ * `.svg` is intentionally absent: it is XML text, so the `@pierre/diffs`
16
+ * text diff (the fallback provider) is more useful for it than an image
17
+ * comparison.
18
+ */
19
+ const IMAGE_DATA_TYPES = {
20
+ '.png': 'png',
21
+ '.jpg': 'jpeg',
22
+ '.jpeg': 'jpeg',
23
+ '.gif': 'gif',
24
+ '.webp': 'webp',
25
+ '.bmp': 'bmp',
26
+ '.ico': 'x-icon'
27
+ };
28
+ /** The file extensions registered as the xtralab image diff provider. */
29
+ export const IMAGE_DIFF_EXTENSIONS = Object.keys(IMAGE_DATA_TYPES);
30
+ /**
31
+ * The `data:` MIME subtype for a path xtralab renders as an image, or
32
+ * `null` when the path is not a supported raster image.
33
+ */
34
+ export function imageDataType(path) {
35
+ var _a;
36
+ const lower = path.toLowerCase();
37
+ const dot = lower.lastIndexOf('.');
38
+ if (dot < 0) {
39
+ return null;
40
+ }
41
+ return (_a = IMAGE_DATA_TYPES[lower.slice(dot)]) !== null && _a !== void 0 ? _a : null;
42
+ }
43
+ const IMAGE_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:image-diff-view-mode';
44
+ function readStoredImageViewMode() {
45
+ try {
46
+ const raw = window.localStorage.getItem(IMAGE_DIFF_VIEW_MODE_STORAGE_KEY);
47
+ if (raw === '2-up' || raw === 'swipe' || raw === 'onion') {
48
+ return raw;
49
+ }
50
+ }
51
+ catch (_a) {
52
+ // localStorage can throw in privacy/sandboxed contexts — 2-up is the
53
+ // sensible default anyway.
54
+ }
55
+ return '2-up';
56
+ }
57
+ function writeStoredImageViewMode(mode) {
58
+ try {
59
+ window.localStorage.setItem(IMAGE_DIFF_VIEW_MODE_STORAGE_KEY, mode);
60
+ }
61
+ catch (_a) {
62
+ // Best-effort.
63
+ }
64
+ }
65
+ /**
66
+ * Strip whitespace from a base64 payload. Python's `base64.encodebytes`
67
+ * (used by the git server) inserts a newline every 76 characters; most
68
+ * browsers tolerate that inside a `data:` URL but some are stricter, and
69
+ * the cleaned length also lets us size the file accurately.
70
+ */
71
+ function cleanBase64(value) {
72
+ return value.replace(/\s+/g, '');
73
+ }
74
+ /** Decoded byte length of a (whitespace-free) base64 string. */
75
+ function base64ByteLength(clean) {
76
+ if (clean.length === 0) {
77
+ return 0;
78
+ }
79
+ const padding = clean.endsWith('==') ? 2 : clean.endsWith('=') ? 1 : 0;
80
+ return Math.floor((clean.length * 3) / 4) - padding;
81
+ }
82
+ function formatBytes(bytes) {
83
+ if (bytes < 1024) {
84
+ return `${bytes} B`;
85
+ }
86
+ const units = ['KB', 'MB', 'GB'];
87
+ let value = bytes / 1024;
88
+ let unit = 0;
89
+ while (value >= 1024 && unit < units.length - 1) {
90
+ value /= 1024;
91
+ unit += 1;
92
+ }
93
+ return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`;
94
+ }
95
+ function toSide(raw, fileType) {
96
+ const data = cleanBase64(raw);
97
+ const present = data.length > 0;
98
+ return {
99
+ data,
100
+ present,
101
+ uri: present ? `data:image/${fileType};base64,${data}` : null,
102
+ bytes: base64ByteLength(data)
103
+ };
104
+ }
105
+ /**
106
+ * The image comparison surface: a segmented mode selector (reusing the
107
+ * diff segmented-control styling so it matches the Notebook/JSON toggle)
108
+ * plus the selected 2-up / swipe / onion-skin view.
109
+ */
110
+ export function ImageDiffView(props) {
111
+ const { reference, challenger, fileType } = props;
112
+ const ref = React.useMemo(() => toSide(reference, fileType), [reference, fileType]);
113
+ const chall = React.useMemo(() => toSide(challenger, fileType), [challenger, fileType]);
114
+ const [mode, setMode] = React.useState(() => readStoredImageViewMode());
115
+ const changeMode = React.useCallback((next) => {
116
+ setMode(next);
117
+ writeStoredImageViewMode(next);
118
+ }, []);
119
+ return (React.createElement("div", { className: "jp-xtralab-ImageDiff" },
120
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-header" },
121
+ React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented", role: "tablist", "aria-label": "Image diff view mode" }, [
122
+ ['2-up', '2-up'],
123
+ ['swipe', 'Swipe'],
124
+ ['onion', 'Onion Skin']
125
+ ].map(([value, label]) => (React.createElement("button", { key: value, type: "button", role: "tab", "aria-selected": mode === value, "data-active": mode === value, className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => changeMode(value) }, label))))),
126
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-body" }, mode === '2-up' ? (React.createElement(TwoUp, { reference: ref, challenger: chall })) : mode === 'swipe' ? (React.createElement(Swipe, { reference: ref, challenger: chall })) : (React.createElement(OnionSkin, { reference: ref, challenger: chall })))));
127
+ }
128
+ function useNaturalSize() {
129
+ const [size, setSize] = React.useState(null);
130
+ const onLoad = React.useCallback((event) => {
131
+ setSize([
132
+ event.currentTarget.naturalWidth,
133
+ event.currentTarget.naturalHeight
134
+ ]);
135
+ }, []);
136
+ return [size, onLoad];
137
+ }
138
+ function caption(side, size, emptyLabel) {
139
+ if (!side.present) {
140
+ return emptyLabel;
141
+ }
142
+ const dims = size !== null ? `${size[0]}×${size[1]} · ` : '';
143
+ return `${dims}${formatBytes(side.bytes)}`;
144
+ }
145
+ function TwoUp({ reference, challenger }) {
146
+ const [refSize, onRefLoad] = useNaturalSize();
147
+ const [challSize, onChallLoad] = useNaturalSize();
148
+ return (React.createElement("div", { className: "jp-xtralab-ImageDiff-twoUp" },
149
+ React.createElement("figure", { className: "jp-xtralab-ImageDiff-col" },
150
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-frame", "data-side": "reference" }, reference.uri !== null ? (React.createElement("img", { className: "jp-xtralab-ImageDiff-img", src: reference.uri, alt: "Reference revision", onLoad: onRefLoad })) : (React.createElement("div", { className: "jp-xtralab-ImageDiff-empty" }, "No previous image"))),
151
+ React.createElement("figcaption", { className: "jp-xtralab-ImageDiff-caption" }, caption(reference, refSize, 'Added'))),
152
+ React.createElement("figure", { className: "jp-xtralab-ImageDiff-col" },
153
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-frame", "data-side": "challenger" }, challenger.uri !== null ? (React.createElement("img", { className: "jp-xtralab-ImageDiff-img", src: challenger.uri, alt: "Challenger revision", onLoad: onChallLoad })) : (React.createElement("div", { className: "jp-xtralab-ImageDiff-empty" }, "Deleted"))),
154
+ React.createElement("figcaption", { className: "jp-xtralab-ImageDiff-caption" }, caption(challenger, challSize, 'Deleted')))));
155
+ }
156
+ /**
157
+ * Range slider shared by the swipe and onion-skin views. A native
158
+ * `<input type="range">` (styled via CSS) keeps the bundle free of a UI
159
+ * component dependency.
160
+ */
161
+ function RangeSlider(props) {
162
+ const { value, onChange, ariaLabel } = props;
163
+ return (React.createElement("input", { className: "jp-xtralab-ImageDiff-slider", type: "range", min: 0, max: 100, step: 1, value: value, "aria-label": ariaLabel, onChange: event => onChange(Number(event.target.value)) }));
164
+ }
165
+ function Swipe({ reference, challenger }) {
166
+ const [position, setPosition] = React.useState(50);
167
+ const [dragging, setDragging] = React.useState(false);
168
+ const frameRef = React.useRef(null);
169
+ const setFromClientX = React.useCallback((clientX) => {
170
+ const frame = frameRef.current;
171
+ if (frame === null) {
172
+ return;
173
+ }
174
+ const rect = frame.getBoundingClientRect();
175
+ if (rect.width <= 0) {
176
+ return;
177
+ }
178
+ const next = ((clientX - rect.left) / rect.width) * 100;
179
+ setPosition(Math.max(0, Math.min(100, next)));
180
+ }, []);
181
+ const onPointerDown = React.useCallback((event) => {
182
+ // Only react to the primary pointer (left click / touch / pen tip).
183
+ if (event.button !== 0) {
184
+ return;
185
+ }
186
+ event.preventDefault();
187
+ setDragging(true);
188
+ setFromClientX(event.clientX);
189
+ }, [setFromClientX]);
190
+ React.useEffect(() => {
191
+ if (!dragging) {
192
+ return;
193
+ }
194
+ const onMove = (event) => {
195
+ setFromClientX(event.clientX);
196
+ };
197
+ const onUp = () => {
198
+ setDragging(false);
199
+ };
200
+ window.addEventListener('pointermove', onMove);
201
+ window.addEventListener('pointerup', onUp);
202
+ window.addEventListener('pointercancel', onUp);
203
+ return () => {
204
+ window.removeEventListener('pointermove', onMove);
205
+ window.removeEventListener('pointerup', onUp);
206
+ window.removeEventListener('pointercancel', onUp);
207
+ };
208
+ }, [dragging, setFromClientX]);
209
+ const onKeyDown = React.useCallback((event) => {
210
+ const step = event.shiftKey ? 10 : 1;
211
+ let next = null;
212
+ switch (event.key) {
213
+ case 'ArrowLeft':
214
+ next = position - step;
215
+ break;
216
+ case 'ArrowRight':
217
+ next = position + step;
218
+ break;
219
+ case 'Home':
220
+ next = 0;
221
+ break;
222
+ case 'End':
223
+ next = 100;
224
+ break;
225
+ }
226
+ if (next === null) {
227
+ return;
228
+ }
229
+ event.preventDefault();
230
+ setPosition(Math.max(0, Math.min(100, next)));
231
+ }, [position]);
232
+ return (React.createElement("div", { className: "jp-xtralab-ImageDiff-interactive" },
233
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-swipeFrame", ref: frameRef, onPointerDown: onPointerDown, "data-dragging": dragging ? 'true' : undefined },
234
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-stack" },
235
+ reference.uri !== null ? (React.createElement("img", { className: "jp-xtralab-ImageDiff-stackImg", src: reference.uri, alt: "Reference revision", draggable: false, style: {
236
+ clipPath: `inset(0 ${100 - position}% 0 0)`
237
+ } })) : null,
238
+ challenger.uri !== null ? (React.createElement("img", { className: "jp-xtralab-ImageDiff-stackImg", src: challenger.uri, alt: "Challenger revision", draggable: false, style: {
239
+ clipPath: `inset(0 0 0 ${position}%)`
240
+ } })) : null),
241
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-swipeDivider", style: { left: `${position}%` }, role: "slider", "aria-label": "Swipe between reference and challenger", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(position), "aria-orientation": "vertical", tabIndex: 0, onKeyDown: onKeyDown },
242
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-swipeDividerHandle jp-mod-top" },
243
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-left" }),
244
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-right" })),
245
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-swipeDividerHandle jp-mod-bottom" },
246
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-left" }),
247
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-right" }))))));
248
+ }
249
+ function OnionSkin({ reference, challenger }) {
250
+ const [opacity, setOpacity] = React.useState(50);
251
+ return (React.createElement("div", { className: "jp-xtralab-ImageDiff-interactive" },
252
+ React.createElement("div", { className: "jp-xtralab-ImageDiff-stack" },
253
+ reference.uri !== null ? (React.createElement("img", { className: "jp-xtralab-ImageDiff-stackImg", src: reference.uri, alt: "Reference revision" })) : null,
254
+ challenger.uri !== null ? (React.createElement("img", { className: "jp-xtralab-ImageDiff-stackImg", src: challenger.uri, alt: "Challenger revision", style: { opacity: opacity / 100 } })) : null),
255
+ React.createElement(RangeSlider, { value: opacity, onChange: setOpacity, ariaLabel: "Fade between reference and challenger" })));
256
+ }
@@ -1,11 +1,13 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ import { CommandArguments, CommandIDs } from './commands';
2
3
  /**
3
- * The git plugin. Adds a VS Code-style "Source Control" panel to the
4
- * JupyterLab left sidebar and opens file diffs in the main area when the
5
- * user clicks an entry in the panel. Backed by the `jupyterlab_git` server
6
- * extension's REST API; the bundled `@jupyterlab/git` frontend is disabled
7
- * via `package.json`'s `jupyterlab.disabledExtensions` so the two panels do
8
- * not coexist.
4
+ * Every plugin contributed by xtralab's git integration:
5
+ *
6
+ * 1. {@link diffCommandPlugin} — the launcher's `xtralab:git:open-diff`.
7
+ * 2. `diffProviderPlugin` — registers the `@pierre/diffs` rendering as
8
+ * jupyterlab-git's diff providers (replacing its disabled
9
+ * notebook/text diff plugins).
9
10
  */
10
- declare const plugin: JupyterFrontEndPlugin<void>;
11
- export default plugin;
11
+ declare const plugins: JupyterFrontEndPlugin<unknown>[];
12
+ export { CommandArguments, CommandIDs };
13
+ export default plugins;
package/lib/git/index.js CHANGED
@@ -1,36 +1,29 @@
1
- import { ILayoutRestorer } from '@jupyterlab/application';
2
- import { Dialog, ICommandPalette, IThemeManager, WidgetTracker, showDialog, showErrorMessage } from '@jupyterlab/apputils';
1
+ import { IThemeManager, WidgetTracker } from '@jupyterlab/apputils';
3
2
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
3
  import { CommandIDs, registerGitCommands } from './commands';
5
4
  import { PREVIEW_DIFF_WIDGET_ID, pinnedDiffWidgetId } from './diffWidget';
6
- import { GitPanel } from './panel';
7
- const GIT_PLUGIN_ID = 'xtralab:git';
5
+ import diffProviderPlugin from './diffProvider';
6
+ const GIT_DIFF_COMMAND_PLUGIN_ID = 'xtralab:git-diff-command';
8
7
  const GIT_DIFF_TRACKER_NAMESPACE = 'xtralab-git-diff';
9
8
  /**
10
- * The git plugin. Adds a VS Code-style "Source Control" panel to the
11
- * JupyterLab left sidebar and opens file diffs in the main area when the
12
- * user clicks an entry in the panel. Backed by the `jupyterlab_git` server
13
- * extension's REST API; the bundled `@jupyterlab/git` frontend is disabled
14
- * via `package.json`'s `jupyterlab.disabledExtensions` so the two panels do
15
- * not coexist.
9
+ * The launcher's git diff command plugin.
10
+ *
11
+ * The `@jupyterlab/git` frontend stays enabled (only its diff plugins are
12
+ * swapped — see `diffProvider.tsx`), so the upstream git panel is what the
13
+ * user sees in the sidebar. This plugin is the launcher dashboard's
14
+ * independent diff path: the `xtralab:git:open-diff` command the
15
+ * dashboard's "Changes" section calls, plus a tracker so reopening a diff
16
+ * reveals the existing tab and the preview/pin behavior keeps working.
16
17
  */
17
- const plugin = {
18
- id: GIT_PLUGIN_ID,
19
- description: 'A VS Code-style git changes panel and diff viewer powered by jupyterlab-git and @pierre/diffs.',
18
+ const diffCommandPlugin = {
19
+ id: GIT_DIFF_COMMAND_PLUGIN_ID,
20
+ description: "The launcher dashboard's side-by-side git diff command, powered by @pierre/diffs.",
20
21
  autoStart: true,
21
- optional: [
22
- ILayoutRestorer,
23
- IThemeManager,
24
- ICommandPalette,
25
- IRenderMimeRegistry
26
- ],
27
- activate: (app, restorer, themeManager, commandPalette, rendermime) => {
28
- // Match the panel's repo path: empty string means "use the JupyterLab
29
- // server's root and let git resolve the enclosing repo".
30
- const repoPath = '';
31
- // Track open diff widgets so the layout restorer can recreate them on
32
- // reload, and so the openDiff handler can reveal an already-open diff
33
- // for the same file instead of creating a duplicate.
22
+ optional: [IThemeManager, IRenderMimeRegistry],
23
+ activate: (app, themeManager, rendermime) => {
24
+ // Track open diff widgets so the openDiff handler can reveal an
25
+ // already-open diff for the same file instead of creating a duplicate
26
+ // (and so a promoted preview can find its pinned twin).
34
27
  const tracker = new WidgetTracker({
35
28
  namespace: GIT_DIFF_TRACKER_NAMESPACE
36
29
  });
@@ -39,36 +32,15 @@ const plugin = {
39
32
  const existing = tracker.find(widget => !widget.isDisposed && widget.id === id);
40
33
  return existing !== null && existing !== void 0 ? existing : undefined;
41
34
  };
42
- const panel = new GitPanel({
43
- openDiff: (change, options) => {
44
- const args = { repoPath, change };
45
- if ((options === null || options === void 0 ? void 0 : options.pin) === true) {
46
- args.pin = true;
47
- }
48
- void app.commands.execute(CommandIDs.openDiff, args);
49
- },
50
- confirm: async (title, body, accept) => {
51
- const result = await showDialog({
52
- title,
53
- body,
54
- buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: accept })]
55
- });
56
- return result.button.accept === true;
57
- },
58
- showError: showErrorMessage
59
- });
60
- app.shell.add(panel, 'left', { rank: 60 });
61
- if (restorer !== null) {
62
- restorer.add(panel, panel.id);
63
- }
64
35
  registerGitCommands({
65
36
  app,
66
- panel,
67
37
  themeManager,
68
- commandPalette,
69
38
  contentsManager: app.serviceManager.contents,
70
39
  rendermime,
71
- onChanged: () => panel.refresh(),
40
+ // The launcher polls git status on its own cadence and the
41
+ // jupyterlab-git panel auto-refreshes on the contents `fileChanged`
42
+ // signal a hunk-discard save emits, so no extra refresh is needed.
43
+ onChanged: () => undefined,
72
44
  trackDiff: widget => tracker.add(widget),
73
45
  onPinned: current => {
74
46
  const existing = findDiff(current.content.change, true);
@@ -87,4 +59,17 @@ const plugin = {
87
59
  });
88
60
  }
89
61
  };
90
- export default plugin;
62
+ /**
63
+ * Every plugin contributed by xtralab's git integration:
64
+ *
65
+ * 1. {@link diffCommandPlugin} — the launcher's `xtralab:git:open-diff`.
66
+ * 2. `diffProviderPlugin` — registers the `@pierre/diffs` rendering as
67
+ * jupyterlab-git's diff providers (replacing its disabled
68
+ * notebook/text diff plugins).
69
+ */
70
+ const plugins = [
71
+ diffCommandPlugin,
72
+ diffProviderPlugin
73
+ ];
74
+ export { CommandIDs };
75
+ export default plugins;
package/lib/index.d.ts CHANGED
@@ -2,9 +2,11 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
  /**
3
3
  * Every plugin contributed by `xtralab`. The entry point of the
4
4
  * labextension is an array because the package bundles several independent
5
- * enhancements (file browser, git changes panel, …) — JupyterLab activates
5
+ * enhancements (file browser, git diff providers, …) — JupyterLab activates
6
6
  * each plugin individually and only the ones whose required tokens are
7
- * available end up running.
7
+ * available end up running. `gitPlugins` and `launcherPlugins` are themselves
8
+ * arrays (the git diff providers; the launcher plus its editor registry), so
9
+ * they are spread in.
8
10
  */
9
11
  declare const plugins: JupyterFrontEndPlugin<unknown>[];
10
12
  export default plugins;
package/lib/index.js CHANGED
@@ -1,26 +1,32 @@
1
+ import agentSessionsPlugin from './agentSessions';
1
2
  import fileBrowserPlugin from './fileBrowser';
2
3
  import editorBreadcrumbsPlugin from './editorBreadcrumbs';
3
4
  import editorIndentPlugin from './editorIndent';
4
- import gitPlugin from './git';
5
- import launcherPlugin from './launcher';
5
+ import gitPlugins from './git';
6
+ import launcherPlugins from './launcher';
6
7
  import menusPlugin from './menus';
7
8
  import sidebarPlugin from './sidebar';
8
- import statusBarPlugin from './statusBar';
9
+ import terminalsPlugin from './terminals';
10
+ import topBarPlugin from './topBar';
9
11
  /**
10
12
  * Every plugin contributed by `xtralab`. The entry point of the
11
13
  * labextension is an array because the package bundles several independent
12
- * enhancements (file browser, git changes panel, …) — JupyterLab activates
14
+ * enhancements (file browser, git diff providers, …) — JupyterLab activates
13
15
  * each plugin individually and only the ones whose required tokens are
14
- * available end up running.
16
+ * available end up running. `gitPlugins` and `launcherPlugins` are themselves
17
+ * arrays (the git diff providers; the launcher plus its editor registry), so
18
+ * they are spread in.
15
19
  */
16
20
  const plugins = [
21
+ agentSessionsPlugin,
17
22
  editorBreadcrumbsPlugin,
18
23
  editorIndentPlugin,
19
24
  fileBrowserPlugin,
20
- gitPlugin,
21
- launcherPlugin,
25
+ ...gitPlugins,
26
+ ...launcherPlugins,
22
27
  menusPlugin,
23
28
  sidebarPlugin,
24
- statusBarPlugin
29
+ terminalsPlugin,
30
+ topBarPlugin
25
31
  ];
26
32
  export default plugins;
@@ -23,7 +23,7 @@ export interface IAgent {
23
23
  * the user types one into the launcher's prompt box. The semantics:
24
24
  *
25
25
  * - `[]` → prompt is appended as a positional argument:
26
- * `<command> 'PROMPT'`. (Used by claude, codex, gemini, vibe.)
26
+ * `<command> 'PROMPT'`. (Used by claude, codex, vibe.)
27
27
  * - `['-i']` / `['--prompt']` → the prompt is preceded by a flag:
28
28
  * `<command> -i 'PROMPT'`. (Used by copilot, opencode.)
29
29
  * - `undefined` → the agent does not accept an initial prompt. The
@@ -1,12 +1,15 @@
1
1
  import { LabIcon, terminalIcon } from '@jupyterlab/ui-components';
2
2
  import { BUILTIN_AGENT_ICONS } from './icons';
3
3
  /**
4
- * Default agent cards shipped with xtralab. The list mirrors the eight
5
- * first-class personas in `jupyter-ai-contrib/jupyter-ai-acp-client` (which
6
- * is also where the icons come from), but uses the bare CLI names a user
7
- * would type in a terminal rather than the ACP wrapper binaries that
8
- * project spawns. Anything not on the user's `$PATH` is filtered out at
9
- * activation time, so the wider list is harmless.
4
+ * Default agent cards shipped with xtralab. Seven of these (Claude, Codex,
5
+ * Copilot, Goose, OpenCode, Kiro, Mistral Vibe) mirror first-class personas
6
+ * in `jupyter-ai-contrib/jupyter-ai-acp-client` (which is also where their
7
+ * icons come from), but use the bare CLI names a user would type in a
8
+ * terminal rather than the ACP wrapper binaries that project spawns.
9
+ * Antigravity (Google's agent-first IDE/CLI, `agy`) takes the Google slot —
10
+ * Gemini CLI was dropped because Google is sunsetting it for consumer
11
+ * accounts on 2026-06-18. Anything not on the user's `$PATH` is filtered
12
+ * out at activation time, so the wider list is harmless.
10
13
  */
11
14
  const DEFAULTS = [
12
15
  {
@@ -30,14 +33,20 @@ const DEFAULTS = [
30
33
  promptArgs: []
31
34
  },
32
35
  {
33
- id: 'gemini',
34
- label: 'Gemini',
35
- caption: 'Start the Gemini CLI in a new terminal.',
36
- command: 'gemini',
37
- icon: BUILTIN_AGENT_ICONS.gemini,
36
+ id: 'antigravity',
37
+ label: 'Antigravity',
38
+ caption: 'Open Google Antigravity from a new terminal.',
39
+ command: 'agy',
40
+ icon: BUILTIN_AGENT_ICONS.antigravity,
38
41
  rank: 2,
39
- requireAvailable: true,
40
- promptArgs: []
42
+ requireAvailable: true
43
+ // `agy` is the Antigravity launcher binary — a VS Code-derived editor
44
+ // CLI that opens the Agent Manager / workspace, not a terminal REPL.
45
+ // Its positional args are file/folder paths, so there is no inline
46
+ // natural-language prompt form; like goose/kiro we omit promptArgs so a
47
+ // typed prompt dims the button instead of being mangled into a path.
48
+ // The command stays the bare binary (no `agy .`) so the server-side
49
+ // `shutil.which` availability check still resolves it.
41
50
  },
42
51
  {
43
52
  id: 'copilot',
@@ -1,15 +1,14 @@
1
1
  import type { JupyterFrontEnd } from '@jupyterlab/application';
2
+ import type { MainAreaWidget } from '@jupyterlab/apputils';
3
+ import type { ITerminal } from '@jupyterlab/terminal';
4
+ import type { CommandRegistry } from '@lumino/commands';
2
5
  import { type IDisposable } from '@lumino/disposable';
6
+ import type { IAgentSessions } from '../agentSessions';
3
7
  import type { IAgent } from './agents';
4
8
  /**
5
9
  * The command id for opening the launcher.
6
10
  */
7
11
  export declare const CREATE_LAUNCHER_COMMAND = "launcher:create";
8
- /**
9
- * Build a JupyterLab command id for a given agent. Centralised so the
10
- * plugin and the launcher both use the same names.
11
- */
12
- export declare function agentCommandId(agentId: string): string;
13
12
  /**
14
13
  * Register a JupyterLab command per agent. Each command opens a new
15
14
  * terminal via `terminal:create-new` and feeds the agent's shell command
@@ -18,5 +17,32 @@ export declare function agentCommandId(agentId: string): string;
18
17
  * Returns a single disposable that tears down every registered command —
19
18
  * the launcher disposes the previous set before re-registering on settings
20
19
  * changes so the command palette stays in sync with the configured agents.
20
+ *
21
+ * When an `agentSessions` registry is supplied, each launch tags its terminal
22
+ * session with the agent's command, so the terminals panel can badge the row
23
+ * with the agent's logo immediately (before server-side detection confirms
24
+ * it).
25
+ */
26
+ export declare function registerAgentCommands(app: JupyterFrontEnd, agents: IAgent[], agentSessions?: IAgentSessions | null): IDisposable;
27
+ /**
28
+ * Open a fresh terminal, type `invocation` into it as if the user had, and
29
+ * label the tab. Shared by the per-agent launch commands and the launcher's
30
+ * editor tile so the fiddly "wait for the websocket, then write stdin"
31
+ * sequence lives in one place.
32
+ *
33
+ * `onSession`, when given, is called once with the new session's name right
34
+ * after the terminal is revealed. Callers use it to optimistically tag the
35
+ * session so the terminals panel can badge it with the agent's (or editor's)
36
+ * logo before server-side detection confirms the running process.
37
+ *
38
+ * Resolves with the host `MainAreaWidget` so the caller can place it — the
39
+ * launcher swaps it into its own tab.
21
40
  */
22
- export declare function registerAgentCommands(app: JupyterFrontEnd, agents: IAgent[]): IDisposable;
41
+ export declare function launchInTerminal(commands: CommandRegistry, options: {
42
+ cwd?: string;
43
+ /** The literal command line typed into the fresh terminal. */
44
+ invocation: string;
45
+ /** Tab/title label applied once the command is sent. */
46
+ label: string;
47
+ onSession?: (sessionName: string) => void;
48
+ }): Promise<MainAreaWidget<ITerminal.ITerminal>>;