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.
- package/README.md +110 -73
- package/lib/agentSessions.d.ts +50 -0
- package/lib/agentSessions.js +45 -0
- package/lib/fileBrowser/index.js +5 -1
- package/lib/git/commands.d.ts +12 -13
- package/lib/git/commands.js +7 -28
- package/lib/git/diffProvider.d.ts +8 -0
- package/lib/git/diffProvider.js +314 -0
- package/lib/git/diffSurface.d.ts +115 -0
- package/lib/git/diffSurface.js +342 -0
- package/lib/git/diffWidget.d.ts +8 -13
- package/lib/git/diffWidget.js +65 -311
- package/lib/git/imageDiff.d.ts +23 -0
- package/lib/git/imageDiff.js +256 -0
- package/lib/git/index.d.ts +10 -8
- package/lib/git/index.js +37 -52
- package/lib/index.d.ts +4 -2
- package/lib/index.js +14 -8
- package/lib/launcher/agents.d.ts +1 -1
- package/lib/launcher/agents.js +22 -13
- package/lib/launcher/commands.d.ts +32 -6
- package/lib/launcher/commands.js +81 -57
- package/lib/launcher/dashboard.d.ts +15 -0
- package/lib/launcher/dashboard.js +119 -5
- package/lib/launcher/editorRegistry.d.ts +43 -0
- package/lib/launcher/editorRegistry.js +96 -0
- package/lib/launcher/editors.d.ts +78 -0
- package/lib/launcher/editors.js +110 -0
- package/lib/launcher/icons.d.ts +25 -6
- package/lib/launcher/icons.js +116 -19
- package/lib/launcher/index.d.ts +2 -22
- package/lib/launcher/index.js +48 -25
- package/lib/launcher/registry.d.ts +21 -0
- package/lib/launcher/registry.js +28 -0
- package/lib/launcher/tokens.d.ts +41 -0
- package/lib/launcher/tokens.js +17 -0
- package/lib/terminals/detection.d.ts +11 -0
- package/lib/terminals/detection.js +50 -0
- package/lib/terminals/index.d.ts +27 -0
- package/lib/terminals/index.js +194 -0
- package/lib/terminals/model.d.ts +191 -0
- package/lib/terminals/model.js +418 -0
- package/lib/terminals/widget.d.ts +72 -0
- package/lib/terminals/widget.js +93 -0
- package/lib/topBar/icons.d.ts +13 -0
- package/lib/topBar/icons.js +27 -0
- package/lib/topBar/index.d.ts +16 -0
- package/lib/topBar/index.js +86 -0
- package/package.json +52 -35
- package/schema/launcher.json +56 -1
- package/src/agentSessions.ts +94 -0
- package/src/fileBrowser/index.ts +5 -1
- package/src/git/commands.ts +12 -38
- package/src/git/diffProvider.tsx +462 -0
- package/src/git/diffSurface.tsx +588 -0
- package/src/git/diffWidget.tsx +105 -465
- package/src/git/imageDiff.tsx +469 -0
- package/src/git/index.ts +38 -68
- package/src/index.ts +14 -8
- package/src/launcher/agents.ts +23 -14
- package/src/launcher/commands.ts +98 -60
- package/src/launcher/dashboard.tsx +205 -3
- package/src/launcher/editorRegistry.ts +149 -0
- package/src/launcher/editors.ts +183 -0
- package/src/launcher/icons.ts +120 -20
- package/src/launcher/index.ts +62 -27
- package/src/launcher/registry.ts +33 -0
- package/src/launcher/tokens.ts +51 -0
- package/src/terminals/detection.ts +65 -0
- package/src/terminals/index.ts +222 -0
- package/src/terminals/model.ts +497 -0
- package/src/terminals/widget.tsx +240 -0
- package/src/topBar/icons.ts +30 -0
- package/src/topBar/index.ts +121 -0
- package/style/git.css +267 -266
- package/style/index.css +2 -1
- package/style/index.js +2 -1
- package/style/launcher.css +124 -2
- package/style/terminals.css +235 -0
- package/style/topBar.css +38 -0
- package/lib/git/icons.d.ts +0 -14
- package/lib/git/icons.js +0 -19
- package/lib/git/panel.d.ts +0 -29
- package/lib/git/panel.js +0 -42
- package/lib/git/panelComponent.d.ts +0 -28
- package/lib/git/panelComponent.js +0 -320
- package/lib/statusBar/index.d.ts +0 -14
- package/lib/statusBar/index.js +0 -89
- package/lib/statusBar/widget.d.ts +0 -121
- package/lib/statusBar/widget.js +0 -237
- package/src/git/icons.ts +0 -20
- package/src/git/panel.tsx +0 -56
- package/src/git/panelComponent.tsx +0 -575
- package/src/statusBar/index.ts +0 -115
- package/src/statusBar/widget.tsx +0 -318
- package/style/statusBar.css +0 -95
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* xtralab's image diff, registered in place of `@jupyterlab/git:image-diff`.
|
|
5
|
+
*
|
|
6
|
+
* The git server base64-encodes binary content, so both diff hosts receive
|
|
7
|
+
* the two sides as base64 strings through the same `content()` getters used
|
|
8
|
+
* for text. This module turns those strings into `<img>` elements and
|
|
9
|
+
* offers the 2-up / swipe / onion-skin comparison modes; no diffing library
|
|
10
|
+
* is involved (`@pierre/diffs` is text-only).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Raster image extensions xtralab renders as an image diff, mapped to the
|
|
15
|
+
* MIME subtype used in the `data:image/<subtype>;base64,…` URL.
|
|
16
|
+
*
|
|
17
|
+
* `.svg` is intentionally absent: it is XML text, so the `@pierre/diffs`
|
|
18
|
+
* text diff (the fallback provider) is more useful for it than an image
|
|
19
|
+
* comparison.
|
|
20
|
+
*/
|
|
21
|
+
const IMAGE_DATA_TYPES: Record<string, string> = {
|
|
22
|
+
'.png': 'png',
|
|
23
|
+
'.jpg': 'jpeg',
|
|
24
|
+
'.jpeg': 'jpeg',
|
|
25
|
+
'.gif': 'gif',
|
|
26
|
+
'.webp': 'webp',
|
|
27
|
+
'.bmp': 'bmp',
|
|
28
|
+
'.ico': 'x-icon'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** The file extensions registered as the xtralab image diff provider. */
|
|
32
|
+
export const IMAGE_DIFF_EXTENSIONS = Object.keys(IMAGE_DATA_TYPES);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The `data:` MIME subtype for a path xtralab renders as an image, or
|
|
36
|
+
* `null` when the path is not a supported raster image.
|
|
37
|
+
*/
|
|
38
|
+
export function imageDataType(path: string): string | null {
|
|
39
|
+
const lower = path.toLowerCase();
|
|
40
|
+
const dot = lower.lastIndexOf('.');
|
|
41
|
+
if (dot < 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return IMAGE_DATA_TYPES[lower.slice(dot)] ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Comparison layouts, matching jupyterlab-git's image diff modes. */
|
|
48
|
+
type ImageDiffViewMode = '2-up' | 'swipe' | 'onion';
|
|
49
|
+
|
|
50
|
+
const IMAGE_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:image-diff-view-mode';
|
|
51
|
+
|
|
52
|
+
function readStoredImageViewMode(): ImageDiffViewMode {
|
|
53
|
+
try {
|
|
54
|
+
const raw = window.localStorage.getItem(IMAGE_DIFF_VIEW_MODE_STORAGE_KEY);
|
|
55
|
+
if (raw === '2-up' || raw === 'swipe' || raw === 'onion') {
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// localStorage can throw in privacy/sandboxed contexts — 2-up is the
|
|
60
|
+
// sensible default anyway.
|
|
61
|
+
}
|
|
62
|
+
return '2-up';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeStoredImageViewMode(mode: ImageDiffViewMode): void {
|
|
66
|
+
try {
|
|
67
|
+
window.localStorage.setItem(IMAGE_DIFF_VIEW_MODE_STORAGE_KEY, mode);
|
|
68
|
+
} catch {
|
|
69
|
+
// Best-effort.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Strip whitespace from a base64 payload. Python's `base64.encodebytes`
|
|
75
|
+
* (used by the git server) inserts a newline every 76 characters; most
|
|
76
|
+
* browsers tolerate that inside a `data:` URL but some are stricter, and
|
|
77
|
+
* the cleaned length also lets us size the file accurately.
|
|
78
|
+
*/
|
|
79
|
+
function cleanBase64(value: string): string {
|
|
80
|
+
return value.replace(/\s+/g, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Decoded byte length of a (whitespace-free) base64 string. */
|
|
84
|
+
function base64ByteLength(clean: string): number {
|
|
85
|
+
if (clean.length === 0) {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
const padding = clean.endsWith('==') ? 2 : clean.endsWith('=') ? 1 : 0;
|
|
89
|
+
return Math.floor((clean.length * 3) / 4) - padding;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatBytes(bytes: number): string {
|
|
93
|
+
if (bytes < 1024) {
|
|
94
|
+
return `${bytes} B`;
|
|
95
|
+
}
|
|
96
|
+
const units = ['KB', 'MB', 'GB'];
|
|
97
|
+
let value = bytes / 1024;
|
|
98
|
+
let unit = 0;
|
|
99
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
100
|
+
value /= 1024;
|
|
101
|
+
unit += 1;
|
|
102
|
+
}
|
|
103
|
+
return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface IImageSide {
|
|
107
|
+
/** Cleaned base64 payload; empty string means the side does not exist. */
|
|
108
|
+
data: string;
|
|
109
|
+
present: boolean;
|
|
110
|
+
uri: string | null;
|
|
111
|
+
bytes: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toSide(raw: string, fileType: string): IImageSide {
|
|
115
|
+
const data = cleanBase64(raw);
|
|
116
|
+
const present = data.length > 0;
|
|
117
|
+
return {
|
|
118
|
+
data,
|
|
119
|
+
present,
|
|
120
|
+
uri: present ? `data:image/${fileType};base64,${data}` : null,
|
|
121
|
+
bytes: base64ByteLength(data)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface IImageDiffViewProps {
|
|
126
|
+
/** Base64 of the reference (old) revision; empty when added. */
|
|
127
|
+
reference: string;
|
|
128
|
+
/** Base64 of the challenger (new) revision; empty when deleted. */
|
|
129
|
+
challenger: string;
|
|
130
|
+
/** MIME subtype from {@link imageDataType}. */
|
|
131
|
+
fileType: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The image comparison surface: a segmented mode selector (reusing the
|
|
136
|
+
* diff segmented-control styling so it matches the Notebook/JSON toggle)
|
|
137
|
+
* plus the selected 2-up / swipe / onion-skin view.
|
|
138
|
+
*/
|
|
139
|
+
export function ImageDiffView(props: IImageDiffViewProps): React.ReactElement {
|
|
140
|
+
const { reference, challenger, fileType } = props;
|
|
141
|
+
const ref = React.useMemo(
|
|
142
|
+
() => toSide(reference, fileType),
|
|
143
|
+
[reference, fileType]
|
|
144
|
+
);
|
|
145
|
+
const chall = React.useMemo(
|
|
146
|
+
() => toSide(challenger, fileType),
|
|
147
|
+
[challenger, fileType]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const [mode, setMode] = React.useState<ImageDiffViewMode>(() =>
|
|
151
|
+
readStoredImageViewMode()
|
|
152
|
+
);
|
|
153
|
+
const changeMode = React.useCallback((next: ImageDiffViewMode) => {
|
|
154
|
+
setMode(next);
|
|
155
|
+
writeStoredImageViewMode(next);
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className="jp-xtralab-ImageDiff">
|
|
160
|
+
<div className="jp-xtralab-ImageDiff-header">
|
|
161
|
+
<div
|
|
162
|
+
className="jp-xtralab-DiffWidget-segmented"
|
|
163
|
+
role="tablist"
|
|
164
|
+
aria-label="Image diff view mode"
|
|
165
|
+
>
|
|
166
|
+
{(
|
|
167
|
+
[
|
|
168
|
+
['2-up', '2-up'],
|
|
169
|
+
['swipe', 'Swipe'],
|
|
170
|
+
['onion', 'Onion Skin']
|
|
171
|
+
] as [ImageDiffViewMode, string][]
|
|
172
|
+
).map(([value, label]) => (
|
|
173
|
+
<button
|
|
174
|
+
key={value}
|
|
175
|
+
type="button"
|
|
176
|
+
role="tab"
|
|
177
|
+
aria-selected={mode === value}
|
|
178
|
+
data-active={mode === value}
|
|
179
|
+
className="jp-xtralab-DiffWidget-segmentedButton"
|
|
180
|
+
onClick={() => changeMode(value)}
|
|
181
|
+
>
|
|
182
|
+
{label}
|
|
183
|
+
</button>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="jp-xtralab-ImageDiff-body">
|
|
188
|
+
{mode === '2-up' ? (
|
|
189
|
+
<TwoUp reference={ref} challenger={chall} />
|
|
190
|
+
) : mode === 'swipe' ? (
|
|
191
|
+
<Swipe reference={ref} challenger={chall} />
|
|
192
|
+
) : (
|
|
193
|
+
<OnionSkin reference={ref} challenger={chall} />
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface ISideViewProps {
|
|
201
|
+
reference: IImageSide;
|
|
202
|
+
challenger: IImageSide;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function useNaturalSize(): [
|
|
206
|
+
[number, number] | null,
|
|
207
|
+
(event: React.SyntheticEvent<HTMLImageElement>) => void
|
|
208
|
+
] {
|
|
209
|
+
const [size, setSize] = React.useState<[number, number] | null>(null);
|
|
210
|
+
const onLoad = React.useCallback(
|
|
211
|
+
(event: React.SyntheticEvent<HTMLImageElement>) => {
|
|
212
|
+
setSize([
|
|
213
|
+
event.currentTarget.naturalWidth,
|
|
214
|
+
event.currentTarget.naturalHeight
|
|
215
|
+
]);
|
|
216
|
+
},
|
|
217
|
+
[]
|
|
218
|
+
);
|
|
219
|
+
return [size, onLoad];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function caption(
|
|
223
|
+
side: IImageSide,
|
|
224
|
+
size: [number, number] | null,
|
|
225
|
+
emptyLabel: string
|
|
226
|
+
): string {
|
|
227
|
+
if (!side.present) {
|
|
228
|
+
return emptyLabel;
|
|
229
|
+
}
|
|
230
|
+
const dims = size !== null ? `${size[0]}×${size[1]} · ` : '';
|
|
231
|
+
return `${dims}${formatBytes(side.bytes)}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function TwoUp({ reference, challenger }: ISideViewProps): React.ReactElement {
|
|
235
|
+
const [refSize, onRefLoad] = useNaturalSize();
|
|
236
|
+
const [challSize, onChallLoad] = useNaturalSize();
|
|
237
|
+
return (
|
|
238
|
+
<div className="jp-xtralab-ImageDiff-twoUp">
|
|
239
|
+
<figure className="jp-xtralab-ImageDiff-col">
|
|
240
|
+
<div className="jp-xtralab-ImageDiff-frame" data-side="reference">
|
|
241
|
+
{reference.uri !== null ? (
|
|
242
|
+
<img
|
|
243
|
+
className="jp-xtralab-ImageDiff-img"
|
|
244
|
+
src={reference.uri}
|
|
245
|
+
alt="Reference revision"
|
|
246
|
+
onLoad={onRefLoad}
|
|
247
|
+
/>
|
|
248
|
+
) : (
|
|
249
|
+
<div className="jp-xtralab-ImageDiff-empty">No previous image</div>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
<figcaption className="jp-xtralab-ImageDiff-caption">
|
|
253
|
+
{caption(reference, refSize, 'Added')}
|
|
254
|
+
</figcaption>
|
|
255
|
+
</figure>
|
|
256
|
+
<figure className="jp-xtralab-ImageDiff-col">
|
|
257
|
+
<div className="jp-xtralab-ImageDiff-frame" data-side="challenger">
|
|
258
|
+
{challenger.uri !== null ? (
|
|
259
|
+
<img
|
|
260
|
+
className="jp-xtralab-ImageDiff-img"
|
|
261
|
+
src={challenger.uri}
|
|
262
|
+
alt="Challenger revision"
|
|
263
|
+
onLoad={onChallLoad}
|
|
264
|
+
/>
|
|
265
|
+
) : (
|
|
266
|
+
<div className="jp-xtralab-ImageDiff-empty">Deleted</div>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
<figcaption className="jp-xtralab-ImageDiff-caption">
|
|
270
|
+
{caption(challenger, challSize, 'Deleted')}
|
|
271
|
+
</figcaption>
|
|
272
|
+
</figure>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Range slider shared by the swipe and onion-skin views. A native
|
|
279
|
+
* `<input type="range">` (styled via CSS) keeps the bundle free of a UI
|
|
280
|
+
* component dependency.
|
|
281
|
+
*/
|
|
282
|
+
function RangeSlider(props: {
|
|
283
|
+
value: number;
|
|
284
|
+
onChange: (value: number) => void;
|
|
285
|
+
ariaLabel: string;
|
|
286
|
+
}): React.ReactElement {
|
|
287
|
+
const { value, onChange, ariaLabel } = props;
|
|
288
|
+
return (
|
|
289
|
+
<input
|
|
290
|
+
className="jp-xtralab-ImageDiff-slider"
|
|
291
|
+
type="range"
|
|
292
|
+
min={0}
|
|
293
|
+
max={100}
|
|
294
|
+
step={1}
|
|
295
|
+
value={value}
|
|
296
|
+
aria-label={ariaLabel}
|
|
297
|
+
onChange={event => onChange(Number(event.target.value))}
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function Swipe({ reference, challenger }: ISideViewProps): React.ReactElement {
|
|
303
|
+
const [position, setPosition] = React.useState(50);
|
|
304
|
+
const [dragging, setDragging] = React.useState(false);
|
|
305
|
+
const frameRef = React.useRef<HTMLDivElement>(null);
|
|
306
|
+
|
|
307
|
+
const setFromClientX = React.useCallback((clientX: number) => {
|
|
308
|
+
const frame = frameRef.current;
|
|
309
|
+
if (frame === null) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const rect = frame.getBoundingClientRect();
|
|
313
|
+
if (rect.width <= 0) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const next = ((clientX - rect.left) / rect.width) * 100;
|
|
317
|
+
setPosition(Math.max(0, Math.min(100, next)));
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
const onPointerDown = React.useCallback(
|
|
321
|
+
(event: React.PointerEvent<HTMLDivElement>) => {
|
|
322
|
+
// Only react to the primary pointer (left click / touch / pen tip).
|
|
323
|
+
if (event.button !== 0) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
event.preventDefault();
|
|
327
|
+
setDragging(true);
|
|
328
|
+
setFromClientX(event.clientX);
|
|
329
|
+
},
|
|
330
|
+
[setFromClientX]
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
React.useEffect(() => {
|
|
334
|
+
if (!dragging) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const onMove = (event: PointerEvent) => {
|
|
338
|
+
setFromClientX(event.clientX);
|
|
339
|
+
};
|
|
340
|
+
const onUp = () => {
|
|
341
|
+
setDragging(false);
|
|
342
|
+
};
|
|
343
|
+
window.addEventListener('pointermove', onMove);
|
|
344
|
+
window.addEventListener('pointerup', onUp);
|
|
345
|
+
window.addEventListener('pointercancel', onUp);
|
|
346
|
+
return () => {
|
|
347
|
+
window.removeEventListener('pointermove', onMove);
|
|
348
|
+
window.removeEventListener('pointerup', onUp);
|
|
349
|
+
window.removeEventListener('pointercancel', onUp);
|
|
350
|
+
};
|
|
351
|
+
}, [dragging, setFromClientX]);
|
|
352
|
+
|
|
353
|
+
const onKeyDown = React.useCallback(
|
|
354
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
355
|
+
const step = event.shiftKey ? 10 : 1;
|
|
356
|
+
let next: number | null = null;
|
|
357
|
+
switch (event.key) {
|
|
358
|
+
case 'ArrowLeft':
|
|
359
|
+
next = position - step;
|
|
360
|
+
break;
|
|
361
|
+
case 'ArrowRight':
|
|
362
|
+
next = position + step;
|
|
363
|
+
break;
|
|
364
|
+
case 'Home':
|
|
365
|
+
next = 0;
|
|
366
|
+
break;
|
|
367
|
+
case 'End':
|
|
368
|
+
next = 100;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
if (next === null) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
setPosition(Math.max(0, Math.min(100, next)));
|
|
376
|
+
},
|
|
377
|
+
[position]
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<div className="jp-xtralab-ImageDiff-interactive">
|
|
382
|
+
<div
|
|
383
|
+
className="jp-xtralab-ImageDiff-swipeFrame"
|
|
384
|
+
ref={frameRef}
|
|
385
|
+
onPointerDown={onPointerDown}
|
|
386
|
+
data-dragging={dragging ? 'true' : undefined}
|
|
387
|
+
>
|
|
388
|
+
<div className="jp-xtralab-ImageDiff-stack">
|
|
389
|
+
{reference.uri !== null ? (
|
|
390
|
+
<img
|
|
391
|
+
className="jp-xtralab-ImageDiff-stackImg"
|
|
392
|
+
src={reference.uri}
|
|
393
|
+
alt="Reference revision"
|
|
394
|
+
draggable={false}
|
|
395
|
+
style={{
|
|
396
|
+
clipPath: `inset(0 ${100 - position}% 0 0)`
|
|
397
|
+
}}
|
|
398
|
+
/>
|
|
399
|
+
) : null}
|
|
400
|
+
{challenger.uri !== null ? (
|
|
401
|
+
<img
|
|
402
|
+
className="jp-xtralab-ImageDiff-stackImg"
|
|
403
|
+
src={challenger.uri}
|
|
404
|
+
alt="Challenger revision"
|
|
405
|
+
draggable={false}
|
|
406
|
+
style={{
|
|
407
|
+
clipPath: `inset(0 0 0 ${position}%)`
|
|
408
|
+
}}
|
|
409
|
+
/>
|
|
410
|
+
) : null}
|
|
411
|
+
</div>
|
|
412
|
+
<div
|
|
413
|
+
className="jp-xtralab-ImageDiff-swipeDivider"
|
|
414
|
+
style={{ left: `${position}%` }}
|
|
415
|
+
role="slider"
|
|
416
|
+
aria-label="Swipe between reference and challenger"
|
|
417
|
+
aria-valuemin={0}
|
|
418
|
+
aria-valuemax={100}
|
|
419
|
+
aria-valuenow={Math.round(position)}
|
|
420
|
+
aria-orientation="vertical"
|
|
421
|
+
tabIndex={0}
|
|
422
|
+
onKeyDown={onKeyDown}
|
|
423
|
+
>
|
|
424
|
+
<div className="jp-xtralab-ImageDiff-swipeDividerHandle jp-mod-top">
|
|
425
|
+
<div className="jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-left" />
|
|
426
|
+
<div className="jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-right" />
|
|
427
|
+
</div>
|
|
428
|
+
<div className="jp-xtralab-ImageDiff-swipeDividerHandle jp-mod-bottom">
|
|
429
|
+
<div className="jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-left" />
|
|
430
|
+
<div className="jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-right" />
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function OnionSkin({
|
|
439
|
+
reference,
|
|
440
|
+
challenger
|
|
441
|
+
}: ISideViewProps): React.ReactElement {
|
|
442
|
+
const [opacity, setOpacity] = React.useState(50);
|
|
443
|
+
return (
|
|
444
|
+
<div className="jp-xtralab-ImageDiff-interactive">
|
|
445
|
+
<div className="jp-xtralab-ImageDiff-stack">
|
|
446
|
+
{reference.uri !== null ? (
|
|
447
|
+
<img
|
|
448
|
+
className="jp-xtralab-ImageDiff-stackImg"
|
|
449
|
+
src={reference.uri}
|
|
450
|
+
alt="Reference revision"
|
|
451
|
+
/>
|
|
452
|
+
) : null}
|
|
453
|
+
{challenger.uri !== null ? (
|
|
454
|
+
<img
|
|
455
|
+
className="jp-xtralab-ImageDiff-stackImg"
|
|
456
|
+
src={challenger.uri}
|
|
457
|
+
alt="Challenger revision"
|
|
458
|
+
style={{ opacity: opacity / 100 }}
|
|
459
|
+
/>
|
|
460
|
+
) : null}
|
|
461
|
+
</div>
|
|
462
|
+
<RangeSlider
|
|
463
|
+
value={opacity}
|
|
464
|
+
onChange={setOpacity}
|
|
465
|
+
ariaLabel="Fade between reference and challenger"
|
|
466
|
+
/>
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
package/src/git/index.ts
CHANGED
|
@@ -1,66 +1,47 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ILayoutRestorer,
|
|
3
2
|
JupyterFrontEnd,
|
|
4
3
|
JupyterFrontEndPlugin
|
|
5
4
|
} from '@jupyterlab/application';
|
|
6
|
-
import {
|
|
7
|
-
Dialog,
|
|
8
|
-
ICommandPalette,
|
|
9
|
-
IThemeManager,
|
|
10
|
-
WidgetTracker,
|
|
11
|
-
showDialog,
|
|
12
|
-
showErrorMessage
|
|
13
|
-
} from '@jupyterlab/apputils';
|
|
5
|
+
import { IThemeManager, WidgetTracker } from '@jupyterlab/apputils';
|
|
14
6
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
15
7
|
|
|
16
|
-
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
17
|
-
|
|
18
8
|
import { CommandArguments, CommandIDs, registerGitCommands } from './commands';
|
|
19
9
|
import {
|
|
20
10
|
DiffContentWidget,
|
|
21
11
|
PREVIEW_DIFF_WIDGET_ID,
|
|
22
12
|
pinnedDiffWidgetId
|
|
23
13
|
} from './diffWidget';
|
|
14
|
+
import diffProviderPlugin from './diffProvider';
|
|
24
15
|
import type { MainAreaWidget } from '@jupyterlab/apputils';
|
|
25
|
-
import { GitPanel } from './panel';
|
|
26
16
|
import { IFileChange } from './tokens';
|
|
27
17
|
|
|
28
|
-
const
|
|
18
|
+
const GIT_DIFF_COMMAND_PLUGIN_ID = 'xtralab:git-diff-command';
|
|
29
19
|
const GIT_DIFF_TRACKER_NAMESPACE = 'xtralab-git-diff';
|
|
30
20
|
|
|
31
21
|
/**
|
|
32
|
-
* The git plugin.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
22
|
+
* The launcher's git diff command plugin.
|
|
23
|
+
*
|
|
24
|
+
* The `@jupyterlab/git` frontend stays enabled (only its diff plugins are
|
|
25
|
+
* swapped — see `diffProvider.tsx`), so the upstream git panel is what the
|
|
26
|
+
* user sees in the sidebar. This plugin is the launcher dashboard's
|
|
27
|
+
* independent diff path: the `xtralab:git:open-diff` command the
|
|
28
|
+
* dashboard's "Changes" section calls, plus a tracker so reopening a diff
|
|
29
|
+
* reveals the existing tab and the preview/pin behavior keeps working.
|
|
38
30
|
*/
|
|
39
|
-
const
|
|
40
|
-
id:
|
|
31
|
+
const diffCommandPlugin: JupyterFrontEndPlugin<void> = {
|
|
32
|
+
id: GIT_DIFF_COMMAND_PLUGIN_ID,
|
|
41
33
|
description:
|
|
42
|
-
'
|
|
34
|
+
"The launcher dashboard's side-by-side git diff command, powered by @pierre/diffs.",
|
|
43
35
|
autoStart: true,
|
|
44
|
-
optional: [
|
|
45
|
-
ILayoutRestorer,
|
|
46
|
-
IThemeManager,
|
|
47
|
-
ICommandPalette,
|
|
48
|
-
IRenderMimeRegistry
|
|
49
|
-
],
|
|
36
|
+
optional: [IThemeManager, IRenderMimeRegistry],
|
|
50
37
|
activate: (
|
|
51
38
|
app: JupyterFrontEnd,
|
|
52
|
-
restorer: ILayoutRestorer | null,
|
|
53
39
|
themeManager: IThemeManager | null,
|
|
54
|
-
commandPalette: ICommandPalette | null,
|
|
55
40
|
rendermime: IRenderMimeRegistry | null
|
|
56
41
|
): void => {
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Track open diff widgets so the layout restorer can recreate them on
|
|
62
|
-
// reload, and so the openDiff handler can reveal an already-open diff
|
|
63
|
-
// for the same file instead of creating a duplicate.
|
|
42
|
+
// Track open diff widgets so the openDiff handler can reveal an
|
|
43
|
+
// already-open diff for the same file instead of creating a duplicate
|
|
44
|
+
// (and so a promoted preview can find its pinned twin).
|
|
64
45
|
const tracker = new WidgetTracker<MainAreaWidget<DiffContentWidget>>({
|
|
65
46
|
namespace: GIT_DIFF_TRACKER_NAMESPACE
|
|
66
47
|
});
|
|
@@ -76,41 +57,15 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
76
57
|
return existing ?? undefined;
|
|
77
58
|
};
|
|
78
59
|
|
|
79
|
-
const panel = new GitPanel({
|
|
80
|
-
openDiff: (change, options) => {
|
|
81
|
-
const args: CommandArguments.IOpenDiff = { repoPath, change };
|
|
82
|
-
if (options?.pin === true) {
|
|
83
|
-
args.pin = true;
|
|
84
|
-
}
|
|
85
|
-
void app.commands.execute(
|
|
86
|
-
CommandIDs.openDiff,
|
|
87
|
-
args as unknown as ReadonlyPartialJSONObject
|
|
88
|
-
);
|
|
89
|
-
},
|
|
90
|
-
confirm: async (title, body, accept) => {
|
|
91
|
-
const result = await showDialog({
|
|
92
|
-
title,
|
|
93
|
-
body,
|
|
94
|
-
buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: accept })]
|
|
95
|
-
});
|
|
96
|
-
return result.button.accept === true;
|
|
97
|
-
},
|
|
98
|
-
showError: showErrorMessage
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
app.shell.add(panel, 'left', { rank: 60 });
|
|
102
|
-
if (restorer !== null) {
|
|
103
|
-
restorer.add(panel, panel.id);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
60
|
registerGitCommands({
|
|
107
61
|
app,
|
|
108
|
-
panel,
|
|
109
62
|
themeManager,
|
|
110
|
-
commandPalette,
|
|
111
63
|
contentsManager: app.serviceManager.contents,
|
|
112
64
|
rendermime,
|
|
113
|
-
|
|
65
|
+
// The launcher polls git status on its own cadence and the
|
|
66
|
+
// jupyterlab-git panel auto-refreshes on the contents `fileChanged`
|
|
67
|
+
// signal a hunk-discard save emits, so no extra refresh is needed.
|
|
68
|
+
onChanged: () => undefined,
|
|
114
69
|
trackDiff: widget => tracker.add(widget),
|
|
115
70
|
onPinned: current => {
|
|
116
71
|
const existing = findDiff(current.content.change, true);
|
|
@@ -132,4 +87,19 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
132
87
|
}
|
|
133
88
|
};
|
|
134
89
|
|
|
135
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Every plugin contributed by xtralab's git integration:
|
|
92
|
+
*
|
|
93
|
+
* 1. {@link diffCommandPlugin} — the launcher's `xtralab:git:open-diff`.
|
|
94
|
+
* 2. `diffProviderPlugin` — registers the `@pierre/diffs` rendering as
|
|
95
|
+
* jupyterlab-git's diff providers (replacing its disabled
|
|
96
|
+
* notebook/text diff plugins).
|
|
97
|
+
*/
|
|
98
|
+
const plugins: JupyterFrontEndPlugin<unknown>[] = [
|
|
99
|
+
diffCommandPlugin,
|
|
100
|
+
diffProviderPlugin
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
export { CommandArguments, CommandIDs };
|
|
104
|
+
|
|
105
|
+
export default plugins;
|
package/src/index.ts
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
2
|
|
|
3
|
+
import agentSessionsPlugin from './agentSessions';
|
|
3
4
|
import fileBrowserPlugin from './fileBrowser';
|
|
4
5
|
import editorBreadcrumbsPlugin from './editorBreadcrumbs';
|
|
5
6
|
import editorIndentPlugin from './editorIndent';
|
|
6
|
-
import
|
|
7
|
-
import
|
|
7
|
+
import gitPlugins from './git';
|
|
8
|
+
import launcherPlugins from './launcher';
|
|
8
9
|
import menusPlugin from './menus';
|
|
9
10
|
import sidebarPlugin from './sidebar';
|
|
10
|
-
import
|
|
11
|
+
import terminalsPlugin from './terminals';
|
|
12
|
+
import topBarPlugin from './topBar';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Every plugin contributed by `xtralab`. The entry point of the
|
|
14
16
|
* labextension is an array because the package bundles several independent
|
|
15
|
-
* enhancements (file browser, git
|
|
17
|
+
* enhancements (file browser, git diff providers, …) — JupyterLab activates
|
|
16
18
|
* each plugin individually and only the ones whose required tokens are
|
|
17
|
-
* available end up running.
|
|
19
|
+
* available end up running. `gitPlugins` and `launcherPlugins` are themselves
|
|
20
|
+
* arrays (the git diff providers; the launcher plus its editor registry), so
|
|
21
|
+
* they are spread in.
|
|
18
22
|
*/
|
|
19
23
|
const plugins: JupyterFrontEndPlugin<unknown>[] = [
|
|
24
|
+
agentSessionsPlugin,
|
|
20
25
|
editorBreadcrumbsPlugin,
|
|
21
26
|
editorIndentPlugin,
|
|
22
27
|
fileBrowserPlugin,
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
...gitPlugins,
|
|
29
|
+
...launcherPlugins,
|
|
25
30
|
menusPlugin,
|
|
26
31
|
sidebarPlugin,
|
|
27
|
-
|
|
32
|
+
terminalsPlugin,
|
|
33
|
+
topBarPlugin
|
|
28
34
|
];
|
|
29
35
|
|
|
30
36
|
export default plugins;
|