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
@@ -1,100 +1,17 @@
1
1
  import * as React from 'react';
2
2
  import { MainAreaWidget } from '@jupyterlab/apputils';
3
3
  import { PathExt } from '@jupyterlab/coreutils';
4
- import { ReactWidget, ToolbarButton, launchIcon, undoIcon } from '@jupyterlab/ui-components';
4
+ import { ReactWidget, ToolbarButton, launchIcon } from '@jupyterlab/ui-components';
5
5
  import { Signal } from '@lumino/signaling';
6
- import { FileDiff } from '@pierre/diffs/react';
7
- import { diffAcceptRejectHunk, parseDiffFromFile } from '@pierre/diffs';
8
6
  import { getTreeIcon } from '../fileBrowser/icons';
9
7
  import { content } from './api';
10
- import { buildNotebookDiff, NotebookDiffView } from './notebookDiff';
11
- /**
12
- * The CSS class added to the diff main-area widget. The selectors that
13
- * style the embedded `@pierre/diffs` viewer hang off this class.
14
- */
15
- export const DIFF_WIDGET_CSS_CLASS = 'jp-xtralab-DiffWidget';
8
+ import { imageDataType } from './imageDiff';
9
+ import { DIFF_WIDGET_CSS_CLASS, DiffSurface, NotebookViewModeControl, isDarkTheme, readStoredNotebookViewMode, writeStoredNotebookViewMode } from './diffSurface';
10
+ export { DIFF_WIDGET_CSS_CLASS };
16
11
  export const PREVIEW_DIFF_WIDGET_ID = 'xtralab:diff:preview';
17
12
  export function pinnedDiffWidgetId(change) {
18
13
  return `xtralab:diff:pinned:${change.group}:${change.path}`;
19
14
  }
20
- /**
21
- * `localStorage` key for the user's last-chosen split ratio. Persisted so
22
- * the preference survives reloads — the library has no built-in way to
23
- * remember per-host UI state, and reopening every diff at 50/50 is
24
- * annoying once the user has dialed in a layout they like.
25
- */
26
- const SPLIT_RATIO_STORAGE_KEY = 'xtralab:diff-split-ratio';
27
- /**
28
- * Bounds on the left-pane fraction. Keeping a few percent on either side
29
- * guarantees both panes remain visible — dragging to 0/100 would hide a
30
- * column and is almost never what the user wants.
31
- */
32
- const MIN_SPLIT_RATIO = 0.1;
33
- const MAX_SPLIT_RATIO = 0.9;
34
- const DEFAULT_SPLIT_RATIO = 0.5;
35
- /**
36
- * CSS injected via the `@pierre/diffs` `unsafeCSS` option. The library
37
- * places it inside the shadow root under `@layer unsafe`, which beats
38
- * the library's own `@layer base` rule that hardcodes
39
- * `grid-template-columns: 1fr 1fr` on the split-mode container.
40
- *
41
- * The override reads from `--xtralab-split-cols`, a custom property we set
42
- * on the host element via the `style` prop. Custom properties inherit
43
- * through the shadow DOM boundary, so adjusting it on the host instantly
44
- * resizes the columns without re-rendering the diff.
45
- */
46
- const SPLIT_RESIZE_CSS = `pre[data-diff-type="split"][data-overflow="scroll"] {
47
- grid-template-columns: var(--xtralab-split-cols, 1fr 1fr);
48
- }`;
49
- function readStoredSplitRatio() {
50
- try {
51
- const raw = window.localStorage.getItem(SPLIT_RATIO_STORAGE_KEY);
52
- if (raw === null) {
53
- return DEFAULT_SPLIT_RATIO;
54
- }
55
- const parsed = Number.parseFloat(raw);
56
- if (Number.isFinite(parsed) &&
57
- parsed >= MIN_SPLIT_RATIO &&
58
- parsed <= MAX_SPLIT_RATIO) {
59
- return parsed;
60
- }
61
- }
62
- catch (_a) {
63
- // localStorage may throw in privacy mode or sandboxed contexts. Falling
64
- // back to the default is fine — the user can resize again on this run.
65
- }
66
- return DEFAULT_SPLIT_RATIO;
67
- }
68
- function writeStoredSplitRatio(ratio) {
69
- try {
70
- window.localStorage.setItem(SPLIT_RATIO_STORAGE_KEY, ratio.toString());
71
- }
72
- catch (_a) {
73
- // See readStoredSplitRatio — best-effort persistence.
74
- }
75
- }
76
- const NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:notebook-diff-view-mode';
77
- function readStoredNotebookViewMode() {
78
- try {
79
- const raw = window.localStorage.getItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY);
80
- if (raw === 'json' || raw === 'notebook') {
81
- return raw;
82
- }
83
- }
84
- catch (_a) {
85
- // See readStoredSplitRatio — privacy-mode / sandboxed contexts can
86
- // throw on access. The notebook view is the better default anyway.
87
- }
88
- return 'notebook';
89
- }
90
- function writeStoredNotebookViewMode(mode) {
91
- try {
92
- window.localStorage.setItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY, mode);
93
- }
94
- catch (_a) {
95
- // Best-effort.
96
- }
97
- }
98
15
  /**
99
16
  * Build the deterministic widget id used for a diff against a particular
100
17
  * file/group pair. Reused across plugin and command code so reopening a
@@ -133,6 +50,12 @@ export function resolveReferences(change) {
133
50
  * usual document chrome — toolbar slot, focus tracker integration, and
134
51
  * spinner-while-loading hooks — and matches the pattern used for other
135
52
  * read-only main-area widgets in the JupyterLab ecosystem.
53
+ *
54
+ * This is the launcher dashboard's own diff path
55
+ * (`xtralab:git:open-diff`), kept independent of `jupyterlab-git`. The
56
+ * actual diff rendering is delegated to the shared {@link DiffSurface};
57
+ * `jupyterlab-git`'s panel reaches the same surface through the providers
58
+ * registered in `diffProvider.tsx`.
136
59
  */
137
60
  export function createGitDiffWidget(options) {
138
61
  var _a;
@@ -310,50 +233,17 @@ export class DiffContentWidget extends ReactWidget {
310
233
  return React.createElement(DiffViewer, { ...this._options, widget: this });
311
234
  }
312
235
  }
313
- /**
314
- * Detect notebook files by extension. We only look at the suffix so the
315
- * caller (the file-change tracker) doesn't need to plumb a content-type
316
- * field through; mistaking another `.ipynb`-named file for a notebook is
317
- * harmless because {@link buildNotebookDiff} validates the JSON shape and
318
- * we fall back to the file diff if parsing fails.
319
- */
320
- function isNotebookPath(path) {
321
- return path.toLowerCase().endsWith('.ipynb');
322
- }
323
- /**
324
- * Decide whether the active JupyterLab theme is a dark theme. Prefers the
325
- * `IThemeManager` token when available, and falls back to sniffing the
326
- * `data-jp-theme-light` attribute that JupyterLab sets on `<body>` so the
327
- * component still works in stripped-down hosts where the token isn't
328
- * provided (lite, certain notebook configurations, …).
329
- */
330
- function isDarkTheme(themeManager) {
331
- if (themeManager !== null && themeManager.theme !== null) {
332
- return !themeManager.isLight(themeManager.theme);
333
- }
334
- return document.body.dataset.jpThemeLight === 'false';
335
- }
336
236
  function DiffViewer(props) {
237
+ var _a;
337
238
  const { repoPath, change, themeManager, contentsManager, rendermime, onChanged, onEmpty, widget } = props;
338
239
  const [state, setState] = React.useState({
339
240
  loading: true,
340
- metadata: null,
341
- notebookDiff: null,
241
+ oldText: '',
242
+ newText: '',
342
243
  serverPath: '',
343
244
  isBinary: false,
344
245
  error: null
345
246
  });
346
- // Split ratio for the diff columns: fraction of width given to the
347
- // deletions (left) pane. Persisted across sessions so the user only
348
- // dials in their layout once.
349
- const [leftRatio, setLeftRatio] = React.useState(() => readStoredSplitRatio());
350
- const wrapperRef = React.useRef(null);
351
- // The drag handler reads the live ratio from a ref so the listeners we
352
- // attach on pointer-down don't capture a stale value across renders.
353
- const leftRatioRef = React.useRef(leftRatio);
354
- React.useEffect(() => {
355
- leftRatioRef.current = leftRatio;
356
- }, [leftRatio]);
357
247
  // Bumping `reloadKey` re-enters the fetch effect. Used after a successful
358
248
  // hunk discard, so the diff reflects the new working-tree contents
359
249
  // without the user having to close and reopen the tab.
@@ -379,9 +269,16 @@ function DiffViewer(props) {
379
269
  // toolbar reads this flag to decide whether to show the
380
270
  // Notebook/JSON selector — and the toolbar bar itself stays hidden
381
271
  // when the file isn't a parseable notebook.
382
- React.useEffect(() => {
383
- widget.setHasNotebookView(state.notebookDiff !== null);
384
- }, [widget, state.notebookDiff]);
272
+ const handleNotebookAvailabilityChange = React.useCallback((available) => {
273
+ widget.setHasNotebookView(available);
274
+ }, [widget]);
275
+ // The shared surface reports its hunk count after every (re)computation;
276
+ // we keep the latest so the auto-close-on-empty effect can fire after a
277
+ // discard empties the diff.
278
+ const [hunkCount, setHunkCount] = React.useState(null);
279
+ const handleMetadataChange = React.useCallback((info) => {
280
+ setHunkCount(info.hunkCount);
281
+ }, []);
385
282
  // Auto-close after a discard that emptied the diff. Gated on `reloadKey > 0`
386
283
  // so a file that opens already-empty (uncommon but possible) is not closed
387
284
  // before the user has had a chance to look at it.
@@ -389,8 +286,7 @@ function DiffViewer(props) {
389
286
  if (!state.loading &&
390
287
  state.error === null &&
391
288
  !state.isBinary &&
392
- state.metadata !== null &&
393
- state.metadata.hunks.length === 0 &&
289
+ hunkCount === 0 &&
394
290
  reloadKey > 0) {
395
291
  onEmpty === null || onEmpty === void 0 ? void 0 : onEmpty();
396
292
  }
@@ -398,7 +294,7 @@ function DiffViewer(props) {
398
294
  state.loading,
399
295
  state.error,
400
296
  state.isBinary,
401
- state.metadata,
297
+ hunkCount,
402
298
  reloadKey,
403
299
  onEmpty
404
300
  ]);
@@ -428,24 +324,28 @@ function DiffViewer(props) {
428
324
  const serverPath = toServerPath(repoPath, change.path);
429
325
  setState({
430
326
  loading: true,
431
- metadata: null,
432
- notebookDiff: null,
327
+ oldText: '',
328
+ newText: '',
433
329
  serverPath,
434
330
  isBinary: false,
435
331
  error: null
436
332
  });
437
333
  const { oldRef, newRef } = resolveReferences(change);
334
+ // Raster images are still binary, but the git server base64-encodes
335
+ // binary content, so we fetch them like text and let the surface render
336
+ // an image diff. Other binary files have no useful textual diff.
337
+ const isImage = imageDataType(change.path) !== null;
438
338
  void (async () => {
439
- var _a, _b, _c, _d;
339
+ var _a, _b, _c;
440
340
  try {
441
- if (change.isBinary === true) {
341
+ if (change.isBinary === true && !isImage) {
442
342
  if (cancelled) {
443
343
  return;
444
344
  }
445
345
  setState({
446
346
  loading: false,
447
- metadata: null,
448
- notebookDiff: null,
347
+ oldText: '',
348
+ newText: '',
449
349
  serverPath,
450
350
  isBinary: true,
451
351
  error: null
@@ -462,31 +362,10 @@ function DiffViewer(props) {
462
362
  if (cancelled) {
463
363
  return;
464
364
  }
465
- const oldText = (_b = oldRes.content) !== null && _b !== void 0 ? _b : '';
466
- const newText = (_c = newRes.content) !== null && _c !== void 0 ? _c : '';
467
- // For .ipynb files we precompute *both* the cell-by-cell
468
- // notebook diff and the raw JSON file diff up front. The user
469
- // toggles between them via the in-widget toolbar, so keeping
470
- // both around (instead of re-fetching on switch) makes the
471
- // toggle feel instant. If parsing fails, `notebookDiff` stays
472
- // null and we silently fall back to the file diff path.
473
- let notebookDiff = null;
474
- if (isNotebookPath(change.path)) {
475
- notebookDiff = buildNotebookDiff({ oldText, newText });
476
- }
477
- const oldFile = {
478
- name: (_d = change.from) !== null && _d !== void 0 ? _d : change.path,
479
- contents: oldText
480
- };
481
- const newFile = {
482
- name: change.path,
483
- contents: newText
484
- };
485
- const metadata = parseDiffFromFile(oldFile, newFile);
486
365
  setState({
487
366
  loading: false,
488
- metadata,
489
- notebookDiff,
367
+ oldText: (_b = oldRes.content) !== null && _b !== void 0 ? _b : '',
368
+ newText: (_c = newRes.content) !== null && _c !== void 0 ? _c : '',
490
369
  serverPath,
491
370
  isBinary: false,
492
371
  error: null
@@ -498,8 +377,8 @@ function DiffViewer(props) {
498
377
  }
499
378
  setState({
500
379
  loading: false,
501
- metadata: null,
502
- notebookDiff: null,
380
+ oldText: '',
381
+ newText: '',
503
382
  serverPath,
504
383
  isBinary: false,
505
384
  error: err instanceof Error ? err.message : String(err)
@@ -513,158 +392,31 @@ function DiffViewer(props) {
513
392
  // Per-hunk discard buttons are only meaningful for unstaged modifications:
514
393
  // staged-vs-HEAD diffs would need an index manipulation we can't do
515
394
  // through the available endpoints, and untracked files have no previous
516
- // version to revert to. The annotation list collapses to empty in those
517
- // cases and the diff renders without action buttons.
395
+ // version to revert to.
518
396
  const canDiscardHunk = change.group === 'unstaged' && change.status !== 'untracked';
519
- const lineAnnotations = React.useMemo(() => {
520
- if (!canDiscardHunk || state.metadata === null) {
521
- return [];
522
- }
523
- return state.metadata.hunks.map((hunk, hunkIndex) => ({
524
- side: 'additions',
525
- // The first line of each hunk in the new file. We anchor the button
526
- // there so it sits at the top of the hunk's body.
527
- lineNumber: hunk.additionStart,
528
- metadata: { hunkIndex }
529
- }));
530
- }, [canDiscardHunk, state.metadata]);
531
- const handleDiscardHunk = React.useCallback(async (hunkIndex) => {
532
- if (state.metadata === null) {
533
- return;
534
- }
535
- const updated = diffAcceptRejectHunk(state.metadata, hunkIndex, 'reject');
536
- // `additionLines` after a 'reject' contains the full new file with
537
- // the hunk reverted back to the old content. The library splits
538
- // file contents with `/(?<=\n)/` (a lookbehind that keeps the
539
- // trailing `\n` on each line), so the entries already carry their
540
- // own line endings — joining with `''` rebuilds the original text
541
- // verbatim, while joining with `'\n'` would double every newline.
542
- const text = updated.additionLines.join('');
543
- try {
397
+ const hunkDiscard = React.useMemo(() => ({
398
+ enabled: canDiscardHunk,
399
+ save: async (text) => {
544
400
  await contentsManager.save(state.serverPath, {
545
401
  type: 'file',
546
402
  format: 'text',
547
403
  content: text
548
404
  });
405
+ },
406
+ onAfterSave: () => {
549
407
  onChanged();
550
408
  setReloadKey(key => key + 1);
551
409
  }
552
- catch (err) {
553
- console.error('xtralab: failed to discard hunk', err);
554
- }
555
- }, [contentsManager, onChanged, state.metadata, state.serverPath]);
556
- const renderAnnotation = React.useCallback((annotation) => {
557
- if (annotation.metadata === undefined) {
558
- return null;
559
- }
560
- const { hunkIndex } = annotation.metadata;
561
- return (React.createElement("div", { className: "jp-xtralab-DiffWidget-hunkAnnotation" },
562
- React.createElement("button", { type: "button", className: "jp-xtralab-DiffWidget-hunkButton", title: "Discard this hunk's changes", "aria-label": "Discard hunk", onClick: () => void handleDiscardHunk(hunkIndex) },
563
- React.createElement(undoIcon.react, { tag: "span", className: "jp-xtralab-DiffWidget-hunkButton-icon", elementSize: "normal" }))));
564
- }, [handleDiscardHunk]);
565
- const handleResizerPointerDown = React.useCallback((event) => {
566
- // Only react to primary-button drags. Right-click / middle-click on
567
- // the handle should pass through to the browser default.
568
- if (event.button !== 0) {
569
- return;
570
- }
571
- const wrapper = wrapperRef.current;
572
- if (wrapper === null) {
573
- return;
574
- }
575
- event.preventDefault();
576
- const handle = event.currentTarget;
577
- handle.setPointerCapture(event.pointerId);
578
- handle.dataset.dragging = 'true';
579
- const rect = wrapper.getBoundingClientRect();
580
- const startX = event.clientX;
581
- const startRatio = leftRatioRef.current;
582
- const onPointerMove = (ev) => {
583
- if (rect.width <= 0) {
584
- return;
585
- }
586
- const next = startRatio + (ev.clientX - startX) / rect.width;
587
- const clamped = Math.max(MIN_SPLIT_RATIO, Math.min(MAX_SPLIT_RATIO, next));
588
- setLeftRatio(clamped);
589
- };
590
- const onPointerEnd = (ev) => {
591
- handle.releasePointerCapture(ev.pointerId);
592
- delete handle.dataset.dragging;
593
- handle.removeEventListener('pointermove', onPointerMove);
594
- handle.removeEventListener('pointerup', onPointerEnd);
595
- handle.removeEventListener('pointercancel', onPointerEnd);
596
- // Only persist on drag-end so a quick drag isn't followed by a
597
- // burst of writes for every intermediate position.
598
- writeStoredSplitRatio(leftRatioRef.current);
599
- };
600
- handle.addEventListener('pointermove', onPointerMove);
601
- handle.addEventListener('pointerup', onPointerEnd);
602
- handle.addEventListener('pointercancel', onPointerEnd);
603
- }, []);
604
- const handleResizerDoubleClick = React.useCallback(() => {
605
- setLeftRatio(DEFAULT_SPLIT_RATIO);
606
- writeStoredSplitRatio(DEFAULT_SPLIT_RATIO);
607
- }, []);
608
- if (state.loading) {
609
- return React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, "Loading diff\u2026");
610
- }
611
- if (state.error !== null) {
612
- return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status", "data-error": "true" }, state.error));
613
- }
614
- if (state.isBinary) {
615
- return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, "Binary file \u2014 diff not supported."));
616
- }
617
- // Decide which view to render. The notebook view requires a successful
618
- // nbformat parse; if that failed, the toolbar is hidden and we only
619
- // show the raw JSON file diff. The toolbar itself is sticky at the top
620
- // of the content area, sitting above the scroll viewport.
621
- const hasNotebookView = state.notebookDiff !== null;
622
- const showNotebookView = hasNotebookView && notebookViewMode === 'notebook';
623
- const showFileDiff = !showNotebookView && state.metadata !== null;
624
- if (!showNotebookView && !showFileDiff) {
625
- return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, "No content to diff."));
626
- }
627
- // Custom property set on the diffs-container host. The library's
628
- // shadow-root rule consumes it via `var(--xtralab-split-cols, …)` (see
629
- // SPLIT_RESIZE_CSS) and recomputes the column tracks instantly. Only
630
- // relevant in the file-diff path; the notebook view doesn't use it.
631
- const leftPercent = leftRatio * 100;
632
- const hostStyle = {
633
- '--xtralab-split-cols': `${leftPercent}% ${100 - leftPercent}%`
634
- };
635
- return (React.createElement("div", { className: "jp-xtralab-DiffWidget-content" },
636
- React.createElement("div", { ref: wrapperRef, className: "jp-xtralab-DiffWidget-body" },
637
- React.createElement("div", { className: "jp-xtralab-DiffWidget-scroll" }, showNotebookView && state.notebookDiff !== null ? (React.createElement(NotebookDiffView, { diff: state.notebookDiff, dark: dark, rendermime: rendermime })) : showFileDiff && state.metadata !== null ? (React.createElement(FileDiff, { fileDiff: state.metadata, lineAnnotations: lineAnnotations, renderAnnotation: renderAnnotation, style: hostStyle,
638
- // Disable the worker pool: JupyterLab's webpack federation
639
- // pipeline doesn't ship the library's
640
- // `@pierre/diffs/worker/worker.js` file at a URL the worker
641
- // bootstrap can resolve from, so leaving the pool enabled
642
- // crashes on instantiation. Running on the main thread is
643
- // fine for the small files git diffs typically operate on.
644
- disableWorkerPool: true, options: {
645
- diffStyle: 'split',
646
- // Drop the file header — the tab title already shows the
647
- // file name and the panel header carries the change
648
- // context.
649
- disableFileHeader: true,
650
- theme: dark ? 'pierre-dark' : 'pierre-light',
651
- themeType: dark ? 'dark' : 'light',
652
- // Inject the column-resize override into the shadow root
653
- // via the library's `@layer unsafe` channel. Keeping the
654
- // string constant lets the library short-circuit the
655
- // re-render path it uses when this option actually
656
- // changes.
657
- unsafeCSS: SPLIT_RESIZE_CSS
658
- } })) : null),
659
- showFileDiff ? (React.createElement("div", { className: "jp-xtralab-DiffWidget-resizer", style: { left: `${leftPercent}%` }, role: "separator", "aria-orientation": "vertical", "aria-valuenow": Math.round(leftPercent), "aria-valuemin": Math.round(MIN_SPLIT_RATIO * 100), "aria-valuemax": Math.round(MAX_SPLIT_RATIO * 100), title: "Drag to resize the diff panes (double-click to reset)", onPointerDown: handleResizerPointerDown, onDoubleClick: handleResizerDoubleClick })) : null)));
410
+ }), [canDiscardHunk, contentsManager, onChanged, state.serverPath]);
411
+ return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: state.isBinary, oldText: state.oldText, newText: state.newText, newName: change.path, oldName: (_a = change.from) !== null && _a !== void 0 ? _a : change.path, dark: dark, rendermime: rendermime, notebookViewMode: notebookViewMode, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, onMetadataChange: handleMetadataChange, hunkDiscard: hunkDiscard }));
660
412
  }
661
413
  /**
662
414
  * Segmented Notebook/JSON selector mounted into the `MainAreaWidget`
663
- * toolbar for `.ipynb` diffs. The selector reads from and writes to the
664
- * `DiffContentWidget` so React state changes (via the diff content) and
665
- * toolbar interactions stay in sync without either side knowing about
666
- * the other. The host toolbar widget's visibility is toggled by
667
- * {@link createGitDiffWidget} based on the same `hasNotebookView`
415
+ * toolbar for `.ipynb` diffs (launcher path). The selector reads from and
416
+ * writes to the {@link DiffContentWidget} so React state changes (via the
417
+ * diff content) and toolbar interactions stay in sync without either side
418
+ * knowing about the other. The host toolbar widget's visibility is toggled
419
+ * by {@link createGitDiffWidget} based on the same `hasNotebookView`
668
420
  * signal, so this control is always meaningful when it appears.
669
421
  */
670
422
  class NotebookViewModeToolbarItem extends ReactWidget {
@@ -674,28 +426,30 @@ class NotebookViewModeToolbarItem extends ReactWidget {
674
426
  this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
675
427
  }
676
428
  render() {
677
- return React.createElement(NotebookViewModeControl, { content: this._content });
429
+ return React.createElement(NotebookViewModeToolbarControl, { content: this._content });
678
430
  }
679
431
  }
680
- function NotebookViewModeControl(props) {
432
+ function NotebookViewModeToolbarControl(props) {
681
433
  const { content } = props;
682
434
  const [mode, setMode] = React.useState(() => content.notebookViewMode);
435
+ const [available, setAvailable] = React.useState(() => content.hasNotebookView);
683
436
  React.useEffect(() => {
684
- const handler = (_sender, next) => {
437
+ const onMode = (_sender, next) => {
685
438
  setMode(next);
686
439
  };
687
- content.notebookViewModeChanged.connect(handler);
440
+ const onAvailable = (_sender, next) => {
441
+ setAvailable(next);
442
+ };
443
+ content.notebookViewModeChanged.connect(onMode);
444
+ content.hasNotebookViewChanged.connect(onAvailable);
688
445
  setMode(content.notebookViewMode);
446
+ setAvailable(content.hasNotebookView);
689
447
  return () => {
690
- content.notebookViewModeChanged.disconnect(handler);
448
+ content.notebookViewModeChanged.disconnect(onMode);
449
+ content.hasNotebookViewChanged.disconnect(onAvailable);
691
450
  };
692
451
  }, [content]);
693
- if (!content.hasNotebookView) {
694
- return React.createElement(React.Fragment, null);
695
- }
696
- return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented", role: "tablist", "aria-label": "Notebook diff view mode" },
697
- React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'notebook', "data-active": mode === 'notebook', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => content.setNotebookViewMode('notebook') }, "Notebook"),
698
- React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'json', "data-active": mode === 'json', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => content.setNotebookViewMode('json') }, "JSON")));
452
+ return (React.createElement(NotebookViewModeControl, { mode: mode, available: available, onChange: next => content.setNotebookViewMode(next) }));
699
453
  }
700
454
  function formatTitle(change) {
701
455
  var _a;
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+ /** The file extensions registered as the xtralab image diff provider. */
3
+ export declare const IMAGE_DIFF_EXTENSIONS: string[];
4
+ /**
5
+ * The `data:` MIME subtype for a path xtralab renders as an image, or
6
+ * `null` when the path is not a supported raster image.
7
+ */
8
+ export declare function imageDataType(path: string): string | null;
9
+ interface IImageDiffViewProps {
10
+ /** Base64 of the reference (old) revision; empty when added. */
11
+ reference: string;
12
+ /** Base64 of the challenger (new) revision; empty when deleted. */
13
+ challenger: string;
14
+ /** MIME subtype from {@link imageDataType}. */
15
+ fileType: string;
16
+ }
17
+ /**
18
+ * The image comparison surface: a segmented mode selector (reusing the
19
+ * diff segmented-control styling so it matches the Notebook/JSON toggle)
20
+ * plus the selected 2-up / swipe / onion-skin view.
21
+ */
22
+ export declare function ImageDiffView(props: IImageDiffViewProps): React.ReactElement;
23
+ export {};