xtralab 0.11.0 → 0.12.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 (162) hide show
  1. package/README.md +37 -229
  2. package/lib/agentSessions.d.ts +6 -2
  3. package/lib/askAgent/editorSelection.d.ts +53 -0
  4. package/lib/askAgent/editorSelection.js +95 -0
  5. package/lib/askAgent/icons.d.ts +7 -0
  6. package/lib/askAgent/icons.js +15 -0
  7. package/lib/askAgent/index.d.ts +32 -0
  8. package/lib/askAgent/index.js +741 -0
  9. package/lib/askAgent/popup.d.ts +83 -0
  10. package/lib/askAgent/popup.js +221 -0
  11. package/lib/askAgent/prompt.d.ts +24 -0
  12. package/lib/askAgent/prompt.js +116 -0
  13. package/lib/askAgent/queue.d.ts +93 -0
  14. package/lib/askAgent/queue.js +207 -0
  15. package/lib/askAgent/queuePanel.d.ts +76 -0
  16. package/lib/askAgent/queuePanel.js +188 -0
  17. package/lib/askAgent/targetPicker.d.ts +58 -0
  18. package/lib/askAgent/targetPicker.js +74 -0
  19. package/lib/askAgent/tokens.d.ts +124 -0
  20. package/lib/askAgent/tokens.js +13 -0
  21. package/lib/commandBar/index.d.ts +12 -8
  22. package/lib/commandBar/index.js +58 -41
  23. package/lib/editorBreadcrumbs/index.js +9 -4
  24. package/lib/editorBreadcrumbs/widget.d.ts +7 -7
  25. package/lib/editorBreadcrumbs/widget.js +6 -8
  26. package/lib/fileBrowser/commands.d.ts +2 -8
  27. package/lib/fileBrowser/commands.js +39 -30
  28. package/lib/fileBrowser/contents.d.ts +8 -3
  29. package/lib/fileBrowser/fileBrowser.d.ts +2 -1
  30. package/lib/fileBrowser/widget.d.ts +5 -9
  31. package/lib/fileBrowser/widget.js +1 -1
  32. package/lib/fileTypeIcons/index.js +5 -2
  33. package/lib/git/api.d.ts +0 -26
  34. package/lib/git/api.js +2 -44
  35. package/lib/git/askRequest.d.ts +23 -0
  36. package/lib/git/askRequest.js +90 -0
  37. package/lib/git/commands.d.ts +14 -2
  38. package/lib/git/commands.js +16 -13
  39. package/lib/git/diffModel.d.ts +1 -9
  40. package/lib/git/diffModel.js +1 -1
  41. package/lib/git/diffProvider.js +8 -3
  42. package/lib/git/diffSurface.d.ts +19 -2
  43. package/lib/git/diffSurface.js +64 -18
  44. package/lib/git/diffTheme.d.ts +0 -4
  45. package/lib/git/diffTheme.js +1 -1
  46. package/lib/git/diffWidget.d.ts +9 -0
  47. package/lib/git/diffWidget.js +38 -10
  48. package/lib/git/imageDiff.d.ts +17 -4
  49. package/lib/git/imageDiff.js +90 -30
  50. package/lib/git/index.js +7 -2
  51. package/lib/git/notebookDiff.d.ts +10 -67
  52. package/lib/git/notebookDiff.js +62 -69
  53. package/lib/git/tokens.d.ts +5 -2
  54. package/lib/highlight/index.js +12 -8
  55. package/lib/index.js +2 -0
  56. package/lib/launcher/agents.d.ts +11 -1
  57. package/lib/launcher/agents.js +26 -8
  58. package/lib/launcher/commands.d.ts +6 -2
  59. package/lib/launcher/dashboard.d.ts +8 -1
  60. package/lib/launcher/dashboard.js +39 -33
  61. package/lib/launcher/editorRegistry.js +5 -3
  62. package/lib/launcher/editors.d.ts +24 -7
  63. package/lib/launcher/editors.js +14 -5
  64. package/lib/launcher/icons.d.ts +0 -37
  65. package/lib/launcher/icons.js +11 -12
  66. package/lib/launcher/index.js +5 -2
  67. package/lib/launcher/invocation.d.ts +0 -15
  68. package/lib/launcher/invocation.js +1 -1
  69. package/lib/launcher/schemaDefaults.d.ts +3 -1
  70. package/lib/launcher/schemaDefaults.js +3 -1
  71. package/lib/mimeData.d.ts +7 -0
  72. package/lib/mimeData.js +16 -0
  73. package/lib/omnibox/index.d.ts +4 -0
  74. package/lib/omnibox/index.js +43 -2
  75. package/lib/omnibox/model.d.ts +43 -14
  76. package/lib/omnibox/model.js +117 -14
  77. package/lib/omnibox/recents.d.ts +65 -0
  78. package/lib/omnibox/recents.js +109 -0
  79. package/lib/omnibox/tokens.d.ts +6 -2
  80. package/lib/omnibox/widget.d.ts +23 -6
  81. package/lib/omnibox/widget.js +26 -7
  82. package/lib/showOutput/index.js +15 -18
  83. package/lib/terminals/agentTerminals.d.ts +63 -0
  84. package/lib/terminals/agentTerminals.js +157 -0
  85. package/lib/terminals/index.d.ts +7 -1
  86. package/lib/terminals/index.js +50 -30
  87. package/lib/terminals/model.d.ts +11 -2
  88. package/lib/terminals/model.js +27 -8
  89. package/lib/terminals/tokens.d.ts +66 -0
  90. package/lib/terminals/tokens.js +7 -0
  91. package/lib/terminals/widget.d.ts +3 -6
  92. package/lib/terminals/widget.js +1 -1
  93. package/lib/topBar/index.js +3 -3
  94. package/lib/walkthrough/index.js +14 -3
  95. package/lib/walkthrough/panel.d.ts +12 -4
  96. package/lib/walkthrough/panel.js +9 -3
  97. package/package.json +49 -51
  98. package/schema/command-bar.json +10 -0
  99. package/schema/launcher.json +4 -4
  100. package/schema/omnibox.json +15 -0
  101. package/src/agentSessions.ts +6 -2
  102. package/src/askAgent/editorSelection.ts +138 -0
  103. package/src/askAgent/icons.ts +16 -0
  104. package/src/askAgent/index.ts +915 -0
  105. package/src/askAgent/popup.tsx +407 -0
  106. package/src/askAgent/prompt.ts +144 -0
  107. package/src/askAgent/queue.ts +269 -0
  108. package/src/askAgent/queuePanel.tsx +418 -0
  109. package/src/askAgent/targetPicker.tsx +209 -0
  110. package/src/askAgent/tokens.ts +139 -0
  111. package/src/commandBar/index.ts +75 -49
  112. package/src/editorBreadcrumbs/index.ts +14 -4
  113. package/src/editorBreadcrumbs/widget.ts +11 -9
  114. package/src/fileBrowser/commands.ts +45 -31
  115. package/src/fileBrowser/contents.ts +7 -3
  116. package/src/fileBrowser/fileBrowser.tsx +1 -1
  117. package/src/fileBrowser/widget.tsx +5 -3
  118. package/src/fileTypeIcons/index.ts +6 -3
  119. package/src/git/api.ts +2 -61
  120. package/src/git/askRequest.ts +113 -0
  121. package/src/git/commands.ts +41 -14
  122. package/src/git/diffModel.ts +1 -1
  123. package/src/git/diffProvider.tsx +11 -3
  124. package/src/git/diffSurface.tsx +114 -22
  125. package/src/git/diffTheme.ts +1 -1
  126. package/src/git/diffWidget.tsx +54 -7
  127. package/src/git/imageDiff.tsx +169 -58
  128. package/src/git/index.ts +10 -2
  129. package/src/git/notebookDiff.tsx +91 -57
  130. package/src/git/tokens.ts +4 -2
  131. package/src/highlight/index.ts +12 -8
  132. package/src/index.ts +2 -0
  133. package/src/launcher/agents.ts +31 -7
  134. package/src/launcher/commands.ts +6 -2
  135. package/src/launcher/dashboard.tsx +86 -38
  136. package/src/launcher/editorRegistry.ts +10 -4
  137. package/src/launcher/editors.ts +34 -9
  138. package/src/launcher/icons.ts +11 -12
  139. package/src/launcher/index.ts +5 -2
  140. package/src/launcher/invocation.ts +1 -1
  141. package/src/launcher/schemaDefaults.ts +3 -1
  142. package/src/mimeData.ts +20 -0
  143. package/src/omnibox/index.ts +48 -1
  144. package/src/omnibox/model.ts +173 -33
  145. package/src/omnibox/recents.ts +155 -0
  146. package/src/omnibox/tokens.ts +6 -2
  147. package/src/omnibox/widget.tsx +54 -13
  148. package/src/showOutput/index.ts +20 -23
  149. package/src/terminals/agentTerminals.ts +238 -0
  150. package/src/terminals/index.ts +54 -30
  151. package/src/terminals/model.ts +28 -7
  152. package/src/terminals/tokens.ts +77 -0
  153. package/src/terminals/widget.tsx +4 -2
  154. package/src/topBar/index.ts +12 -6
  155. package/src/walkthrough/index.ts +19 -4
  156. package/src/walkthrough/panel.ts +15 -5
  157. package/style/askAgent.css +544 -0
  158. package/style/commandBar.css +12 -33
  159. package/style/git.css +72 -31
  160. package/style/index.css +1 -0
  161. package/style/index.js +2 -0
  162. package/style/topBar.css +4 -4
@@ -4,8 +4,11 @@ import {
4
4
  } from '@jupyterlab/application';
5
5
  import { IThemeManager } from '@jupyterlab/apputils';
6
6
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
7
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
7
8
  import { Git, IGitExtension } from '@jupyterlab/git';
8
9
 
10
+ import { IAskAgent } from '../askAgent/tokens';
11
+
9
12
  import {
10
13
  IXtralabDiffContext,
11
14
  XtralabDiffWidget,
@@ -34,17 +37,22 @@ const diffProviderPlugin: JupyterFrontEndPlugin<void> = {
34
37
  "Replaces jupyterlab-git's notebook, text and image diff plugins with xtralab's renderer.",
35
38
  autoStart: true,
36
39
  requires: [IGitExtension],
37
- optional: [IRenderMimeRegistry, IThemeManager],
40
+ optional: [IRenderMimeRegistry, IThemeManager, ITranslator, IAskAgent],
38
41
  activate: (
39
42
  app: JupyterFrontEnd,
40
43
  gitExtension: IGitExtension,
41
44
  rendermime: IRenderMimeRegistry | null,
42
- themeManager: IThemeManager | null
45
+ themeManager: IThemeManager | null,
46
+ translator: ITranslator | null,
47
+ askAgent: IAskAgent | null
43
48
  ): void => {
49
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
44
50
  const factory = makeXtralabDiffFactory({
45
51
  contentsManager: app.serviceManager.contents,
46
52
  rendermime,
47
- themeManager
53
+ themeManager,
54
+ askAgent,
55
+ trans
48
56
  });
49
57
 
50
58
  // Use one shared factory; the surface detects notebooks/images by filename.
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import { IThemeManager } from '@jupyterlab/apputils';
4
4
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
5
+ import type { TranslationBundle } from '@jupyterlab/translation';
5
6
  import { undoIcon } from '@jupyterlab/ui-components';
6
7
  import { FileDiff } from '@pierre/diffs/react';
7
8
  import {
@@ -9,7 +10,8 @@ import {
9
10
  parseDiffFromFile,
10
11
  type DiffLineAnnotation,
11
12
  type FileContents,
12
- type FileDiffMetadata
13
+ type FileDiffMetadata,
14
+ type SelectedLineRange
13
15
  } from '@pierre/diffs';
14
16
 
15
17
  import { ImageDiffView, imageDataType } from './imageDiff';
@@ -197,13 +199,13 @@ export function isPierreTheme(themeManager: IThemeManager | null): boolean {
197
199
  * the on-disk path) and triggers any follow-up refresh in
198
200
  * {@link IHunkDiscard.onAfterSave}.
199
201
  */
200
- export interface IHunkDiscard {
202
+ interface IHunkDiscard {
201
203
  enabled: boolean;
202
204
  save: (fullText: string) => Promise<void>;
203
205
  onAfterSave: () => void;
204
206
  }
205
207
 
206
- export interface IDiffSurfaceProps {
208
+ interface IDiffSurfaceProps {
207
209
  /**
208
210
  * Whether the host is still resolving the file contents.
209
211
  */
@@ -278,6 +280,18 @@ export interface IDiffSurfaceProps {
278
280
  * Optional per-hunk discard wiring; omit for a read-only diff.
279
281
  */
280
282
  hunkDiscard?: IHunkDiscard;
283
+ /**
284
+ * When provided, the textual/code file diff gets line selection: clicking
285
+ * or dragging over the line numbers selects a range, and a gutter "+"
286
+ * button appears on the hovered/selected lines. Clicking that button
287
+ * invokes this callback with the selected range and the button's viewport
288
+ * rectangle (to anchor a popup to), `null` when it cannot be measured.
289
+ */
290
+ onLineAsk?: (range: SelectedLineRange, anchor: DOMRect | null) => void;
291
+ /**
292
+ * Translation bundle for user-facing strings.
293
+ */
294
+ trans: TranslationBundle;
281
295
  }
282
296
 
283
297
  /**
@@ -300,7 +314,9 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
300
314
  onNotebookAvailabilityChange,
301
315
  onFileDiffActiveChange,
302
316
  onMetadataChange,
303
- hunkDiscard
317
+ hunkDiscard,
318
+ onLineAsk,
319
+ trans
304
320
  } = props;
305
321
 
306
322
  const hasContent = !loading && error === null && !isBinary;
@@ -432,8 +448,8 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
432
448
  <button
433
449
  type="button"
434
450
  className="jp-xtralab-DiffWidget-hunkButton"
435
- title="Discard this hunk's changes"
436
- aria-label="Discard hunk"
451
+ title={trans.__("Discard this hunk's changes")}
452
+ aria-label={trans.__('Discard hunk')}
437
453
  onClick={() => void handleDiscardHunk(hunkIndex)}
438
454
  >
439
455
  <undoIcon.react
@@ -445,7 +461,26 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
445
461
  </div>
446
462
  );
447
463
  },
448
- [handleDiscardHunk]
464
+ [handleDiscardHunk, trans]
465
+ );
466
+
467
+ const handleGutterUtilityClick = React.useCallback(
468
+ (range: SelectedLineRange) => {
469
+ if (onLineAsk === undefined) {
470
+ return;
471
+ }
472
+ // Anchor to the gutter "+" button that was just clicked. It lives in
473
+ // the `@pierre/diffs` shadow root (which is open), parked on the
474
+ // hovered/selected line's number element.
475
+ const slot = wrapperRef.current
476
+ ?.querySelector('diffs-container')
477
+ ?.shadowRoot?.querySelector('[data-gutter-utility-slot]');
478
+ onLineAsk(
479
+ range,
480
+ slot instanceof Element ? slot.getBoundingClientRect() : null
481
+ );
482
+ },
483
+ [onLineAsk]
449
484
  );
450
485
 
451
486
  const handleResizerPointerDown = React.useCallback(
@@ -500,8 +535,44 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
500
535
  writeStoredSplitRatio(DEFAULT_SPLIT_RATIO);
501
536
  }, []);
502
537
 
538
+ const handleResizerKeyDown = React.useCallback(
539
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
540
+ const step = event.shiftKey ? 0.1 : 0.01;
541
+ let next: number | null = null;
542
+ switch (event.key) {
543
+ case 'ArrowLeft':
544
+ next = leftRatioRef.current - step;
545
+ break;
546
+ case 'ArrowRight':
547
+ next = leftRatioRef.current + step;
548
+ break;
549
+ case 'Home':
550
+ next = MIN_SPLIT_RATIO;
551
+ break;
552
+ case 'End':
553
+ next = MAX_SPLIT_RATIO;
554
+ break;
555
+ }
556
+ if (next === null) {
557
+ return;
558
+ }
559
+ event.preventDefault();
560
+ const clamped = Math.max(
561
+ MIN_SPLIT_RATIO,
562
+ Math.min(MAX_SPLIT_RATIO, next)
563
+ );
564
+ setLeftRatio(clamped);
565
+ writeStoredSplitRatio(clamped);
566
+ },
567
+ []
568
+ );
569
+
503
570
  if (loading) {
504
- return <div className="jp-xtralab-DiffWidget-status">Loading diff…</div>;
571
+ return (
572
+ <div className="jp-xtralab-DiffWidget-status">
573
+ {trans.__('Loading diff…')}
574
+ </div>
575
+ );
505
576
  }
506
577
  if (error !== null) {
507
578
  return (
@@ -513,7 +584,7 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
513
584
  if (isBinary) {
514
585
  return (
515
586
  <div className="jp-xtralab-DiffWidget-status">
516
- Binary file — diff not supported.
587
+ {trans.__('Binary file — diff not supported.')}
517
588
  </div>
518
589
  );
519
590
  }
@@ -527,13 +598,16 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
527
598
  reference={oldText}
528
599
  challenger={newText}
529
600
  fileType={imageType}
601
+ trans={trans}
530
602
  />
531
603
  </div>
532
604
  );
533
605
  }
534
606
  if (!showNotebookView && !showFileDiff) {
535
607
  return (
536
- <div className="jp-xtralab-DiffWidget-status">No content to diff.</div>
608
+ <div className="jp-xtralab-DiffWidget-status">
609
+ {trans.__('No content to diff.')}
610
+ </div>
537
611
  );
538
612
  }
539
613
 
@@ -556,6 +630,7 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
556
630
  dark={dark}
557
631
  pierreTheme={pierreTheme}
558
632
  rendermime={rendermime}
633
+ trans={trans}
559
634
  />
560
635
  ) : showFileDiff && metadata !== null ? (
561
636
  <FileDiff<IHunkActionAnnotation>
@@ -583,7 +658,17 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
583
658
  // string constant lets the library short-circuit the
584
659
  // re-render path it uses when this option actually
585
660
  // changes.
586
- unsafeCSS: SPLIT_RESIZE_CSS
661
+ unsafeCSS: SPLIT_RESIZE_CSS,
662
+ // Line selection + the gutter "+" button feed the
663
+ // ask-agent popup; only wired up when a handler exists so
664
+ // a plain diff keeps its passive gutter.
665
+ ...(onLineAsk !== undefined
666
+ ? {
667
+ enableLineSelection: true,
668
+ enableGutterUtility: true,
669
+ onGutterUtilityClick: handleGutterUtilityClick
670
+ }
671
+ : {})
587
672
  }}
588
673
  />
589
674
  ) : null}
@@ -597,9 +682,14 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
597
682
  aria-valuenow={Math.round(leftPercent)}
598
683
  aria-valuemin={Math.round(MIN_SPLIT_RATIO * 100)}
599
684
  aria-valuemax={Math.round(MAX_SPLIT_RATIO * 100)}
600
- title="Drag to resize the diff panes (double-click to reset)"
685
+ aria-label={trans.__('Resize the diff panes')}
686
+ tabIndex={0}
687
+ title={trans.__(
688
+ 'Drag to resize the diff panes (double-click to reset)'
689
+ )}
601
690
  onPointerDown={handleResizerPointerDown}
602
691
  onDoubleClick={handleResizerDoubleClick}
692
+ onKeyDown={handleResizerKeyDown}
603
693
  />
604
694
  ) : null}
605
695
  </div>
@@ -617,8 +707,9 @@ export function NotebookViewModeControl(props: {
617
707
  mode: NotebookDiffViewMode;
618
708
  available: boolean;
619
709
  onChange: (mode: NotebookDiffViewMode) => void;
710
+ trans: TranslationBundle;
620
711
  }): React.ReactElement {
621
- const { mode, available, onChange } = props;
712
+ const { mode, available, onChange, trans } = props;
622
713
  if (!available) {
623
714
  return <></>;
624
715
  }
@@ -626,7 +717,7 @@ export function NotebookViewModeControl(props: {
626
717
  <div
627
718
  className="jp-xtralab-DiffWidget-segmented"
628
719
  role="tablist"
629
- aria-label="Notebook diff view mode"
720
+ aria-label={trans.__('Notebook diff view mode')}
630
721
  >
631
722
  <button
632
723
  type="button"
@@ -636,7 +727,7 @@ export function NotebookViewModeControl(props: {
636
727
  className="jp-xtralab-DiffWidget-segmentedButton"
637
728
  onClick={() => onChange('notebook')}
638
729
  >
639
- Notebook
730
+ {trans.__('Notebook')}
640
731
  </button>
641
732
  <button
642
733
  type="button"
@@ -646,7 +737,7 @@ export function NotebookViewModeControl(props: {
646
737
  className="jp-xtralab-DiffWidget-segmentedButton"
647
738
  onClick={() => onChange('json')}
648
739
  >
649
- JSON
740
+ {trans.__('JSON')}
650
741
  </button>
651
742
  </div>
652
743
  );
@@ -700,8 +791,9 @@ export function DiffStyleControl(props: {
700
791
  diffStyle: DiffStyle;
701
792
  available: boolean;
702
793
  onChange: (style: DiffStyle) => void;
794
+ trans: TranslationBundle;
703
795
  }): React.ReactElement {
704
- const { diffStyle, available, onChange } = props;
796
+ const { diffStyle, available, onChange, trans } = props;
705
797
  if (!available) {
706
798
  return <></>;
707
799
  }
@@ -709,7 +801,7 @@ export function DiffStyleControl(props: {
709
801
  <div
710
802
  className="jp-xtralab-DiffWidget-segmented"
711
803
  role="tablist"
712
- aria-label="Diff view style"
804
+ aria-label={trans.__('Diff view style')}
713
805
  >
714
806
  <button
715
807
  type="button"
@@ -717,8 +809,8 @@ export function DiffStyleControl(props: {
717
809
  aria-selected={diffStyle === 'split'}
718
810
  data-active={diffStyle === 'split'}
719
811
  className="jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon"
720
- title="Split (side-by-side) view"
721
- aria-label="Split view"
812
+ title={trans.__('Split (side-by-side) view')}
813
+ aria-label={trans.__('Split view')}
722
814
  onClick={() => onChange('split')}
723
815
  >
724
816
  <DiffSplitIcon />
@@ -729,8 +821,8 @@ export function DiffStyleControl(props: {
729
821
  aria-selected={diffStyle === 'unified'}
730
822
  data-active={diffStyle === 'unified'}
731
823
  className="jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon"
732
- title="Unified (inline) view"
733
- aria-label="Unified view"
824
+ title={trans.__('Unified (inline) view')}
825
+ aria-label={trans.__('Unified view')}
734
826
  onClick={() => onChange('unified')}
735
827
  >
736
828
  <DiffUnifiedIcon />
@@ -6,7 +6,7 @@ import {
6
6
  /**
7
7
  * Shiki CSS-variable theme used for non-Pierre JupyterLab themes.
8
8
  */
9
- export const JUPYTERLAB_DIFF_THEME: DiffsThemeNames = 'jupyterlab';
9
+ const JUPYTERLAB_DIFF_THEME: DiffsThemeNames = 'jupyterlab';
10
10
 
11
11
  /**
12
12
  * Register a Shiki theme whose token colors read JupyterLab's editor
@@ -5,11 +5,16 @@ import { PathExt } from '@jupyterlab/coreutils';
5
5
  import { Git } from '@jupyterlab/git';
6
6
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
7
7
  import { Contents } from '@jupyterlab/services';
8
+ import type { TranslationBundle } from '@jupyterlab/translation';
8
9
  import { ReactWidget } from '@jupyterlab/ui-components';
9
10
  import { PromiseDelegate } from '@lumino/coreutils';
10
11
  import { ISignal, Signal } from '@lumino/signaling';
11
12
  import { Widget } from '@lumino/widgets';
13
+ import type { SelectedLineRange } from '@pierre/diffs';
12
14
 
15
+ import type { IAskAgent } from '../askAgent/tokens';
16
+
17
+ import { buildDiffAskRequest } from './askRequest';
13
18
  import {
14
19
  DIFF_WIDGET_CSS_CLASS,
15
20
  DiffStyleControl,
@@ -44,6 +49,12 @@ export interface IXtralabDiffContext {
44
49
  contentsManager: Contents.IManager;
45
50
  rendermime: IRenderMimeRegistry | null;
46
51
  themeManager: IThemeManager | null;
52
+ /**
53
+ * The ask-agent popup; when available, diff line selections get an
54
+ * "ask an agent about these lines" gutter button.
55
+ */
56
+ askAgent: IAskAgent | null;
57
+ trans: TranslationBundle;
47
58
  }
48
59
 
49
60
  /**
@@ -103,6 +114,9 @@ export class XtralabDiffWidget
103
114
  * Re-pull both sides of the model and re-render.
104
115
  */
105
116
  async refresh(): Promise<void> {
117
+ // Settle any refresh still awaiting so a rapid second call cannot orphan
118
+ // the earlier caller's promise.
119
+ this.settleRefresh();
106
120
  const done = new PromiseDelegate<void>();
107
121
  this._pendingRefresh = done;
108
122
  this._reloadNonce += 1;
@@ -196,6 +210,14 @@ export class XtralabDiffWidget
196
210
  pending?.resolve();
197
211
  }
198
212
 
213
+ dispose(): void {
214
+ // Release any in-flight refresh awaiter so a host that awaits refresh()
215
+ // (jupyterlab-git re-shows its diff button only once it resolves) does not
216
+ // hang when the widget is torn down mid-fetch.
217
+ this.settleRefresh();
218
+ super.dispose();
219
+ }
220
+
199
221
  get reloadNonce(): number {
200
222
  return this._reloadNonce;
201
223
  }
@@ -237,7 +259,8 @@ function ModelDiffView(props: {
237
259
  }): React.ReactElement {
238
260
  const { widget } = props;
239
261
  const model = widget.model as IXtralabDiffModel;
240
- const { contentsManager, rendermime, themeManager } = widget.context;
262
+ const { contentsManager, rendermime, themeManager, askAgent, trans } =
263
+ widget.context;
241
264
  const nonce = widget.reloadNonce;
242
265
 
243
266
  const [state, setState] = React.useState<IModelDiffState>({
@@ -252,7 +275,7 @@ function ModelDiffView(props: {
252
275
  React.useState<NotebookDiffViewMode>(() => widget.notebookViewMode);
253
276
  React.useEffect(() => {
254
277
  const handler = (
255
- _sender: XtralabDiffWidget,
278
+ sender: XtralabDiffWidget,
256
279
  mode: NotebookDiffViewMode
257
280
  ): void => {
258
281
  setNotebookViewMode(mode);
@@ -276,7 +299,7 @@ function ModelDiffView(props: {
276
299
  () => widget.diffStyle
277
300
  );
278
301
  React.useEffect(() => {
279
- const handler = (_sender: XtralabDiffWidget, style: DiffStyle): void => {
302
+ const handler = (sender: XtralabDiffWidget, style: DiffStyle): void => {
280
303
  setDiffStyle(style);
281
304
  };
282
305
  widget.diffStyleChanged.connect(handler);
@@ -428,6 +451,24 @@ function ModelDiffView(props: {
428
451
  [canDiscardHunk, contentsManager, serverPath, widget]
429
452
  );
430
453
 
454
+ const handleLineAsk = React.useMemo(() => {
455
+ if (askAgent === null) {
456
+ return undefined;
457
+ }
458
+ return (range: SelectedLineRange, anchor: DOMRect | null): void => {
459
+ askAgent.open(
460
+ buildDiffAskRequest({
461
+ model,
462
+ oldText: state.oldText,
463
+ newText: state.newText,
464
+ range,
465
+ anchor,
466
+ trans
467
+ })
468
+ );
469
+ };
470
+ }, [askAgent, model, state.oldText, state.newText, trans]);
471
+
431
472
  return (
432
473
  <DiffSurface
433
474
  loading={state.loading}
@@ -446,6 +487,8 @@ function ModelDiffView(props: {
446
487
  onFileDiffActiveChange={handleFileDiffActiveChange}
447
488
  onMetadataChange={handleMetadataChange}
448
489
  hunkDiscard={hunkDiscard}
490
+ onLineAsk={handleLineAsk}
491
+ trans={trans}
449
492
  />
450
493
  );
451
494
  }
@@ -479,12 +522,12 @@ function NotebookViewModeToolbarControl(props: {
479
522
  );
480
523
  React.useEffect(() => {
481
524
  const onMode = (
482
- _sender: XtralabDiffWidget,
525
+ sender: XtralabDiffWidget,
483
526
  next: NotebookDiffViewMode
484
527
  ): void => {
485
528
  setMode(next);
486
529
  };
487
- const onAvailable = (_sender: XtralabDiffWidget, next: boolean): void => {
530
+ const onAvailable = (sender: XtralabDiffWidget, next: boolean): void => {
488
531
  setAvailable(next);
489
532
  };
490
533
  widget.notebookViewModeChanged.connect(onMode);
@@ -497,11 +540,13 @@ function NotebookViewModeToolbarControl(props: {
497
540
  };
498
541
  }, [widget]);
499
542
 
543
+ const trans = widget.context.trans;
500
544
  return (
501
545
  <NotebookViewModeControl
502
546
  mode={mode}
503
547
  available={available}
504
548
  onChange={next => widget.setNotebookViewMode(next)}
549
+ trans={trans}
505
550
  />
506
551
  );
507
552
  }
@@ -532,10 +577,10 @@ function DiffStyleToolbarControl(props: {
532
577
  () => widget.fileDiffActive
533
578
  );
534
579
  React.useEffect(() => {
535
- const onStyle = (_sender: XtralabDiffWidget, next: DiffStyle): void => {
580
+ const onStyle = (sender: XtralabDiffWidget, next: DiffStyle): void => {
536
581
  setStyle(next);
537
582
  };
538
- const onAvailable = (_sender: XtralabDiffWidget, next: boolean): void => {
583
+ const onAvailable = (sender: XtralabDiffWidget, next: boolean): void => {
539
584
  setAvailable(next);
540
585
  };
541
586
  widget.diffStyleChanged.connect(onStyle);
@@ -548,11 +593,13 @@ function DiffStyleToolbarControl(props: {
548
593
  };
549
594
  }, [widget]);
550
595
 
596
+ const trans = widget.context.trans;
551
597
  return (
552
598
  <DiffStyleControl
553
599
  diffStyle={style}
554
600
  available={available}
555
601
  onChange={next => widget.setDiffStyle(next)}
602
+ trans={trans}
556
603
  />
557
604
  );
558
605
  }