xtralab 0.11.1 → 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 (159) hide show
  1. package/README.md +37 -231
  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.js +3 -4
  57. package/lib/launcher/commands.d.ts +6 -2
  58. package/lib/launcher/dashboard.d.ts +8 -1
  59. package/lib/launcher/dashboard.js +39 -33
  60. package/lib/launcher/editorRegistry.js +5 -3
  61. package/lib/launcher/editors.d.ts +15 -5
  62. package/lib/launcher/icons.d.ts +0 -37
  63. package/lib/launcher/icons.js +11 -12
  64. package/lib/launcher/index.js +2 -1
  65. package/lib/launcher/invocation.d.ts +0 -15
  66. package/lib/launcher/invocation.js +1 -1
  67. package/lib/launcher/schemaDefaults.d.ts +3 -1
  68. package/lib/launcher/schemaDefaults.js +3 -1
  69. package/lib/mimeData.d.ts +7 -0
  70. package/lib/mimeData.js +16 -0
  71. package/lib/omnibox/index.d.ts +4 -0
  72. package/lib/omnibox/index.js +43 -2
  73. package/lib/omnibox/model.d.ts +43 -14
  74. package/lib/omnibox/model.js +117 -14
  75. package/lib/omnibox/recents.d.ts +65 -0
  76. package/lib/omnibox/recents.js +109 -0
  77. package/lib/omnibox/tokens.d.ts +6 -2
  78. package/lib/omnibox/widget.d.ts +23 -6
  79. package/lib/omnibox/widget.js +26 -7
  80. package/lib/showOutput/index.js +15 -18
  81. package/lib/terminals/agentTerminals.d.ts +63 -0
  82. package/lib/terminals/agentTerminals.js +157 -0
  83. package/lib/terminals/index.d.ts +7 -1
  84. package/lib/terminals/index.js +50 -30
  85. package/lib/terminals/model.d.ts +11 -2
  86. package/lib/terminals/model.js +27 -8
  87. package/lib/terminals/tokens.d.ts +66 -0
  88. package/lib/terminals/tokens.js +7 -0
  89. package/lib/terminals/widget.d.ts +3 -6
  90. package/lib/terminals/widget.js +1 -1
  91. package/lib/topBar/index.js +3 -3
  92. package/lib/walkthrough/index.js +14 -3
  93. package/lib/walkthrough/panel.d.ts +12 -4
  94. package/lib/walkthrough/panel.js +9 -3
  95. package/package.json +49 -51
  96. package/schema/command-bar.json +10 -0
  97. package/schema/omnibox.json +15 -0
  98. package/src/agentSessions.ts +6 -2
  99. package/src/askAgent/editorSelection.ts +138 -0
  100. package/src/askAgent/icons.ts +16 -0
  101. package/src/askAgent/index.ts +915 -0
  102. package/src/askAgent/popup.tsx +407 -0
  103. package/src/askAgent/prompt.ts +144 -0
  104. package/src/askAgent/queue.ts +269 -0
  105. package/src/askAgent/queuePanel.tsx +418 -0
  106. package/src/askAgent/targetPicker.tsx +209 -0
  107. package/src/askAgent/tokens.ts +139 -0
  108. package/src/commandBar/index.ts +75 -49
  109. package/src/editorBreadcrumbs/index.ts +14 -4
  110. package/src/editorBreadcrumbs/widget.ts +11 -9
  111. package/src/fileBrowser/commands.ts +45 -31
  112. package/src/fileBrowser/contents.ts +7 -3
  113. package/src/fileBrowser/fileBrowser.tsx +1 -1
  114. package/src/fileBrowser/widget.tsx +5 -3
  115. package/src/fileTypeIcons/index.ts +6 -3
  116. package/src/git/api.ts +2 -61
  117. package/src/git/askRequest.ts +113 -0
  118. package/src/git/commands.ts +41 -14
  119. package/src/git/diffModel.ts +1 -1
  120. package/src/git/diffProvider.tsx +11 -3
  121. package/src/git/diffSurface.tsx +114 -22
  122. package/src/git/diffTheme.ts +1 -1
  123. package/src/git/diffWidget.tsx +54 -7
  124. package/src/git/imageDiff.tsx +169 -58
  125. package/src/git/index.ts +10 -2
  126. package/src/git/notebookDiff.tsx +91 -57
  127. package/src/git/tokens.ts +4 -2
  128. package/src/highlight/index.ts +12 -8
  129. package/src/index.ts +2 -0
  130. package/src/launcher/agents.ts +3 -4
  131. package/src/launcher/commands.ts +6 -2
  132. package/src/launcher/dashboard.tsx +86 -38
  133. package/src/launcher/editorRegistry.ts +10 -4
  134. package/src/launcher/editors.ts +15 -5
  135. package/src/launcher/icons.ts +11 -12
  136. package/src/launcher/index.ts +2 -1
  137. package/src/launcher/invocation.ts +1 -1
  138. package/src/launcher/schemaDefaults.ts +3 -1
  139. package/src/mimeData.ts +20 -0
  140. package/src/omnibox/index.ts +48 -1
  141. package/src/omnibox/model.ts +173 -33
  142. package/src/omnibox/recents.ts +155 -0
  143. package/src/omnibox/tokens.ts +6 -2
  144. package/src/omnibox/widget.tsx +54 -13
  145. package/src/showOutput/index.ts +20 -23
  146. package/src/terminals/agentTerminals.ts +238 -0
  147. package/src/terminals/index.ts +54 -30
  148. package/src/terminals/model.ts +28 -7
  149. package/src/terminals/tokens.ts +77 -0
  150. package/src/terminals/widget.tsx +4 -2
  151. package/src/topBar/index.ts +12 -6
  152. package/src/walkthrough/index.ts +19 -4
  153. package/src/walkthrough/panel.ts +15 -5
  154. package/style/askAgent.css +544 -0
  155. package/style/commandBar.css +12 -33
  156. package/style/git.css +72 -31
  157. package/style/index.css +1 -0
  158. package/style/index.js +2 -0
  159. package/style/topBar.css +4 -4
@@ -59,9 +59,9 @@ export namespace CommandIDs {
59
59
  * core's populator does not try to fill ours with items derived from the
60
60
  * default file browser's selection.
61
61
  */
62
- export const OPEN_WITH_SUBMENU_ID = 'jp-contextmenu-xtralab-open-with';
62
+ const OPEN_WITH_SUBMENU_ID = 'jp-contextmenu-xtralab-open-with';
63
63
 
64
- export interface IRegisterCommandsOptions {
64
+ interface IRegisterCommandsOptions {
65
65
  app: JupyterFrontEnd;
66
66
  browser: IXtralabFileBrowser;
67
67
  docManager: IDocumentManager;
@@ -132,7 +132,9 @@ function getOpenPaths(
132
132
  return candidates.filter(path => !path.endsWith('/'));
133
133
  }
134
134
 
135
- /** True iff there is an actionable target for a command on the right-click. */
135
+ /**
136
+ * True iff there is an actionable target for a command on the right-click.
137
+ */
136
138
  function hasTarget(
137
139
  app: JupyterFrontEnd,
138
140
  browser: IXtralabFileBrowser
@@ -270,7 +272,9 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
270
272
 
271
273
  commands.addCommand(CommandIDs.open, {
272
274
  label: args =>
273
- ((args.label as string) ?? (args.factory as string) ?? 'Open') as string,
275
+ ((args.label as string) ??
276
+ (args.factory as string) ??
277
+ trans.__('Open')) as string,
274
278
  icon: args => {
275
279
  const factoryName = args.factory as string | undefined;
276
280
  if (factoryName !== undefined) {
@@ -299,7 +303,7 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
299
303
  });
300
304
 
301
305
  commands.addCommand(CommandIDs.openBrowserTab, {
302
- label: 'Open in New Browser Tab',
306
+ label: trans.__('Open in New Browser Tab'),
303
307
  icon: fileIcon.bindprops({ stylesheet: 'menuItem' }),
304
308
  isEnabled: () => getTargetKind(app) === 'file',
305
309
  isVisible: () => getTargetKind(app) === 'file',
@@ -315,7 +319,7 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
315
319
  });
316
320
 
317
321
  commands.addCommand(CommandIDs.rename, {
318
- label: 'Rename…',
322
+ label: trans.__('Rename…'),
319
323
  icon: editIcon.bindprops({ stylesheet: 'menuItem' }),
320
324
  mnemonic: 0,
321
325
  isEnabled: () => hasTarget(app, browser),
@@ -327,10 +331,10 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
327
331
  const serverPath = toServerPath(targetPath);
328
332
  const oldName = PathExt.basename(serverPath);
329
333
  const result = await InputDialog.getText({
330
- title: 'Rename',
331
- label: 'Enter a new name',
334
+ title: trans.__('Rename'),
335
+ label: trans.__('Enter a new name'),
332
336
  text: oldName,
333
- okLabel: 'Rename'
337
+ okLabel: trans.__('Rename')
334
338
  });
335
339
  const newName = result.value?.trim();
336
340
  if (
@@ -346,7 +350,7 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
346
350
  await browser.contentsManager.rename(serverPath, newPath);
347
351
  browser.refresh();
348
352
  } catch (err) {
349
- await showErrorMessage('Rename failed', err as Error);
353
+ await showErrorMessage(trans.__('Rename failed'), err as Error);
350
354
  }
351
355
  }
352
356
  });
@@ -354,8 +358,8 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
354
358
  commands.addCommand(CommandIDs.del, {
355
359
  label: () =>
356
360
  PageConfig.getOption('delete_to_trash') === 'true'
357
- ? 'Move to Trash'
358
- : 'Delete',
361
+ ? trans.__('Move to Trash')
362
+ : trans.__('Delete'),
359
363
  icon: closeIcon.bindprops({ stylesheet: 'menuItem' }),
360
364
  mnemonic: 0,
361
365
  isEnabled: () => hasTarget(app, browser),
@@ -367,11 +371,18 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
367
371
  const serverPath = toServerPath(targetPath);
368
372
  const trashing = PageConfig.getOption('delete_to_trash') === 'true';
369
373
  const result = await showDialog({
370
- title: trashing ? 'Move to Trash' : 'Delete',
371
- body: `Are you sure you want to permanently delete: ${serverPath}?`,
374
+ title: trashing ? trans.__('Move to Trash') : trans.__('Delete'),
375
+ body: trashing
376
+ ? trans.__('Are you sure you want to move to trash: %1?', serverPath)
377
+ : trans.__(
378
+ 'Are you sure you want to permanently delete: %1?',
379
+ serverPath
380
+ ),
372
381
  buttons: [
373
382
  Dialog.cancelButton(),
374
- Dialog.warnButton({ label: trashing ? 'Move to Trash' : 'Delete' })
383
+ Dialog.warnButton({
384
+ label: trashing ? trans.__('Move to Trash') : trans.__('Delete')
385
+ })
375
386
  ]
376
387
  });
377
388
  if (result.button.accept !== true) {
@@ -381,13 +392,13 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
381
392
  await browser.contentsManager.delete(serverPath);
382
393
  browser.refresh();
383
394
  } catch (err) {
384
- await showErrorMessage('Delete failed', err as Error);
395
+ await showErrorMessage(trans.__('Delete failed'), err as Error);
385
396
  }
386
397
  }
387
398
  });
388
399
 
389
400
  commands.addCommand(CommandIDs.duplicate, {
390
- label: 'Duplicate',
401
+ label: trans.__('Duplicate'),
391
402
  icon: copyIcon.bindprops({ stylesheet: 'menuItem' }),
392
403
  isEnabled: () => getTargetKind(app) === 'file',
393
404
  isVisible: () => getTargetKind(app) === 'file',
@@ -402,13 +413,13 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
402
413
  const created = await browser.contentsManager.copy(serverPath, dir);
403
414
  browser.notifyPathAdded(toCanonicalPath(created));
404
415
  } catch (err) {
405
- await showErrorMessage('Duplicate failed', err as Error);
416
+ await showErrorMessage(trans.__('Duplicate failed'), err as Error);
406
417
  }
407
418
  }
408
419
  });
409
420
 
410
421
  commands.addCommand(CommandIDs.copyPath, {
411
- label: 'Copy Path',
422
+ label: trans.__('Copy Path'),
412
423
  icon: fileIcon.bindprops({ stylesheet: 'menuItem' }),
413
424
  isEnabled: () => hasTarget(app, browser),
414
425
  execute: async () => {
@@ -431,7 +442,7 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
431
442
  });
432
443
 
433
444
  commands.addCommand(CommandIDs.download, {
434
- label: 'Download',
445
+ label: trans.__('Download'),
435
446
  icon: downloadIcon.bindprops({ stylesheet: 'menuItem' }),
436
447
  isEnabled: () => getTargetKind(app) === 'file',
437
448
  isVisible: () => getTargetKind(app) === 'file',
@@ -450,14 +461,14 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
450
461
  anchor.click();
451
462
  document.body.removeChild(anchor);
452
463
  } catch (err) {
453
- await showErrorMessage('Download failed', err as Error);
464
+ await showErrorMessage(trans.__('Download failed'), err as Error);
454
465
  }
455
466
  }
456
467
  });
457
468
 
458
469
  commands.addCommand(CommandIDs.refresh, {
459
- label: 'Refresh File List',
460
- caption: 'Refresh the file browser',
470
+ label: trans.__('Refresh File List'),
471
+ caption: trans.__('Refresh the file browser'),
461
472
  icon: refreshIcon.bindprops({ stylesheet: 'menuItem' }),
462
473
  execute: () => {
463
474
  browser.refresh();
@@ -465,8 +476,8 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
465
476
  });
466
477
 
467
478
  commands.addCommand(CommandIDs.collapseAll, {
468
- label: 'Collapse All Folders',
469
- caption: 'Collapse all folders in the file browser',
479
+ label: trans.__('Collapse All Folders'),
480
+ caption: trans.__('Collapse all folders in the file browser'),
470
481
  icon: collapseAllIcon.bindprops({ stylesheet: 'menuItem' }),
471
482
  execute: () => {
472
483
  browser.collapseAll();
@@ -482,7 +493,7 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
482
493
  // there is no tree row for the root itself, so the browser is asked
483
494
  // to scroll back to the top and clear its selection instead.
484
495
  commands.addCommand(CommandIDs.revealPath, {
485
- label: 'Reveal in File Browser',
496
+ label: trans.__('Reveal in File Browser'),
486
497
  execute: (args: ReadonlyPartialJSONObject) => {
487
498
  const path = (args.path as string | undefined) ?? '';
488
499
  app.shell.activateById(FILE_BROWSER_ID);
@@ -512,8 +523,8 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
512
523
  });
513
524
 
514
525
  commands.addCommand(CommandIDs.createNewDirectory, {
515
- label: 'New Folder',
516
- caption: 'Create a new folder',
526
+ label: trans.__('New Folder'),
527
+ caption: trans.__('Create a new folder'),
517
528
  icon: newFolderIcon.bindprops({ stylesheet: 'menuItem' }),
518
529
  execute: async () => {
519
530
  const cwd = getWorkingDirectory(browser);
@@ -524,14 +535,17 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
524
535
  });
525
536
  browser.notifyPathAdded(toCanonicalPath(created));
526
537
  } catch (err) {
527
- await showErrorMessage('Could not create folder', err as Error);
538
+ await showErrorMessage(
539
+ trans.__('Could not create folder'),
540
+ err as Error
541
+ );
528
542
  }
529
543
  }
530
544
  });
531
545
 
532
546
  commands.addCommand(CommandIDs.newLauncher, {
533
- label: 'New Launcher',
534
- caption: 'Open a new launcher',
547
+ label: trans.__('New Launcher'),
548
+ caption: trans.__('Open a new launcher'),
535
549
  // Skip the `menuItem` bindprops the other commands use: this command
536
550
  // lives on the toolbar, where the launcher-extension's blue button
537
551
  // styling expects the raw `jp-icon3` paths from `addIcon` so it can
@@ -30,10 +30,14 @@ export function toServerPath(canonicalPath: string): string {
30
30
  : canonicalPath;
31
31
  }
32
32
 
33
- export interface IListedDirectory {
34
- /** Canonical paths for every immediate child of the requested directory. */
33
+ interface IListedDirectory {
34
+ /**
35
+ * Canonical paths for every immediate child of the requested directory.
36
+ */
35
37
  paths: string[];
36
- /** Subset of {@link IListedDirectory.paths} that are directories. */
38
+ /**
39
+ * Subset of {@link IListedDirectory.paths} that are directories.
40
+ */
37
41
  subdirectories: string[];
38
42
  }
39
43
 
@@ -99,7 +99,7 @@ const FILE_TREE_UNSAFE_CSS =
99
99
  'border-left-color: transparent;' +
100
100
  '}';
101
101
 
102
- export interface IFileBrowserProps {
102
+ interface IFileBrowserProps {
103
103
  contentsManager: Contents.IManager;
104
104
  docManager: IDocumentManager;
105
105
  onOpenFile?: (serverPath: string) => void;
@@ -17,12 +17,12 @@ export const FILE_BROWSER_ID = 'xtralab:file-browser';
17
17
  * specific enough not to clash with other browsers (such as the default
18
18
  * `jp-FileBrowser`).
19
19
  */
20
- export const FILE_BROWSER_CSS_CLASS = 'jp-xtralab-FileBrowser';
20
+ const FILE_BROWSER_CSS_CLASS = 'jp-xtralab-FileBrowser';
21
21
 
22
22
  const TOOLBAR_CSS_CLASS = 'jp-xtralab-FileBrowser-toolbar';
23
23
  const CONTENT_CSS_CLASS = 'jp-xtralab-FileBrowser-content';
24
24
 
25
- export interface IXtralabFileBrowserOptions {
25
+ interface IXtralabFileBrowserOptions {
26
26
  contentsManager: Contents.IManager;
27
27
  docManager: IDocumentManager;
28
28
  onOpenFile?: (serverPath: string) => void;
@@ -99,7 +99,9 @@ export interface IXtralabFileBrowser {
99
99
  */
100
100
  readonly collapseAllRequested: ISignal<IXtralabFileBrowser, void>;
101
101
 
102
- /** Trigger a refresh of every loaded directory in the tree. */
102
+ /**
103
+ * Trigger a refresh of every loaded directory in the tree.
104
+ */
103
105
  refresh(): void;
104
106
 
105
107
  /**
@@ -148,8 +148,12 @@ const plugin: JupyterFrontEndPlugin<void> = {
148
148
  }
149
149
  };
150
150
  setIcon();
151
- // Keep the tab icon right after a rename.
151
+ // Keep the tab icon right after a rename, and release the listener
152
+ // when the document closes.
152
153
  widget.context.pathChanged.connect(setIcon);
154
+ widget.disposed.connect(() => {
155
+ widget.context.pathChanged.disconnect(setIcon);
156
+ });
153
157
  });
154
158
  }
155
159
 
@@ -163,8 +167,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
163
167
  if (icon === null) {
164
168
  continue;
165
169
  }
166
- // Cross jupyterlab-git's nested `LabIcon` package boundary.
167
- file.type = { ...file.type, icon } as unknown as typeof file.type;
170
+ file.type = { ...file.type, icon };
168
171
  }
169
172
  });
170
173
  }
package/src/git/api.ts CHANGED
@@ -49,7 +49,7 @@ async function postWithPath<T>(
49
49
  if (text.length > 0) {
50
50
  try {
51
51
  data = JSON.parse(text) as T;
52
- } catch (_err) {
52
+ } catch {
53
53
  // Fall through into the error handling below.
54
54
  }
55
55
  }
@@ -61,34 +61,6 @@ async function postWithPath<T>(
61
61
  return (data ?? ({} as T)) as T;
62
62
  }
63
63
 
64
- /**
65
- * Fetch the absolute filesystem path of the git top-level for the given
66
- * server-relative path. Returns `null` when the path is not in a git
67
- * repository — the server returns a non-zero `code` (or `path: null`) in
68
- * that case, which is expected and not a fatal error.
69
- */
70
- export async function showTopLevel(repoPath: string): Promise<string | null> {
71
- interface IShowTopLevelResult {
72
- code: number;
73
- path?: string | null;
74
- message?: string;
75
- }
76
- let result: IShowTopLevelResult;
77
- try {
78
- result = await postWithPath<IShowTopLevelResult>(
79
- 'show_top_level',
80
- repoPath,
81
- {}
82
- );
83
- } catch (err) {
84
- return null;
85
- }
86
- if (result.code !== 0 || !result.path) {
87
- return null;
88
- }
89
- return result.path;
90
- }
91
-
92
64
  /**
93
65
  * Fetch `git status --porcelain` for the repository the server resolves at
94
66
  * `repoPath`. When `repoPath` is the empty string, git resolves the repo
@@ -114,37 +86,6 @@ export async function content(
114
86
  });
115
87
  }
116
88
 
117
- /** Stage a single file. */
118
- export async function add(repoPath: string, filename: string): Promise<void> {
119
- await postWithPath('add', repoPath, { add_all: false, filename });
120
- }
121
-
122
- /** Stage every changed file (including untracked). */
123
- export async function addAll(repoPath: string): Promise<void> {
124
- await postWithPath('add', repoPath, { add_all: true });
125
- }
126
-
127
- /** Unstage a single file. */
128
- export async function reset(repoPath: string, filename: string): Promise<void> {
129
- await postWithPath('reset', repoPath, { reset_all: false, filename });
130
- }
131
-
132
- /** Unstage every file. */
133
- export async function resetAll(repoPath: string): Promise<void> {
134
- await postWithPath('reset', repoPath, { reset_all: true });
135
- }
136
-
137
- /** Restore a single file to the version in the index (i.e. discard worktree changes). */
138
- export async function checkout(
139
- repoPath: string,
140
- filename: string
141
- ): Promise<void> {
142
- await postWithPath('checkout', repoPath, {
143
- checkout_branch: false,
144
- filename
145
- });
146
- }
147
-
148
89
  /**
149
90
  * Expand the porcelain `files` array into one entry per logical change.
150
91
  * A single porcelain row may map to two `IFileChange` rows (a file that's
@@ -173,7 +114,7 @@ export function expandStatusFiles(files: IGitStatusFile[]): IFileChange[] {
173
114
  * shows up twice in the panel — once under "Staged Changes" and once under
174
115
  * "Changes" — matching the layout the user sees in VS Code.
175
116
  */
176
- export function porcelainToFileChanges(file: IGitStatusFile): IFileChange[] {
117
+ function porcelainToFileChanges(file: IGitStatusFile): IFileChange[] {
177
118
  const result: IFileChange[] = [];
178
119
  // Untracked files are reported with `??` and only appear under "Changes".
179
120
  if (file.x === '?' && file.y === '?') {
@@ -0,0 +1,113 @@
1
+ import { Git } from '@jupyterlab/git';
2
+ import type { TranslationBundle } from '@jupyterlab/translation';
3
+ import type { SelectedLineRange } from '@pierre/diffs';
4
+
5
+ import type { IAskAgentRequest } from '../askAgent/tokens';
6
+
7
+ import type { IXtralabDiffModel } from './diffWidget';
8
+
9
+ type DiffSide = 'deletions' | 'additions';
10
+
11
+ /**
12
+ * Agent-facing name of a diff content source: the special refs get plain
13
+ * words, a git ref string is used verbatim, anything else falls back to the
14
+ * side name. English on purpose — this ends up in the agent prompt, not in
15
+ * the UI.
16
+ */
17
+ function describeSource(source: unknown, fallback: string): string {
18
+ if (source === Git.Diff.SpecialRef.WORKING) {
19
+ return 'working tree';
20
+ }
21
+ if (source === Git.Diff.SpecialRef.INDEX) {
22
+ return 'staged';
23
+ }
24
+ if (source === Git.Diff.SpecialRef.BASE) {
25
+ return 'merge base';
26
+ }
27
+ if (typeof source === 'string' && source.length > 0) {
28
+ return source;
29
+ }
30
+ return fallback;
31
+ }
32
+
33
+ function sideName(side: DiffSide): string {
34
+ return side === 'deletions' ? 'old side' : 'new side';
35
+ }
36
+
37
+ /**
38
+ * Extract lines `startLine`–`endLine` (1-indexed, inclusive) from `text`.
39
+ */
40
+ function sliceLines(text: string, startLine: number, endLine: number): string {
41
+ return text
42
+ .split('\n')
43
+ .slice(startLine - 1, endLine)
44
+ .join('\n');
45
+ }
46
+
47
+ /**
48
+ * Build the ask-agent request for a line range selected in a git diff.
49
+ *
50
+ * The range's `side` decides which file version the line numbers (and the
51
+ * embedded snippet) refer to: `deletions` reads from the reference text,
52
+ * anything else — including the sideless context rows of a unified diff,
53
+ * whose gutter shows new-side numbers — reads from the challenger text. A
54
+ * gutter drag that crosses the two split columns has endpoints in different
55
+ * line-number spaces, so it is described textually instead of as a numeric
56
+ * range.
57
+ */
58
+ export function buildDiffAskRequest(options: {
59
+ model: IXtralabDiffModel;
60
+ oldText: string;
61
+ newText: string;
62
+ range: SelectedLineRange;
63
+ anchor: DOMRect | null;
64
+ trans: TranslationBundle;
65
+ }): IAskAgentRequest {
66
+ const { model, oldText, newText, range, anchor, trans } = options;
67
+ const repositoryPath = model.repositoryPath ?? '';
68
+ const base = {
69
+ path: model.filename,
70
+ ...(repositoryPath.length > 0 ? { cwd: repositoryPath } : {})
71
+ };
72
+
73
+ const startSide: DiffSide = range.side ?? 'additions';
74
+ const endSide: DiffSide = range.endSide ?? startSide;
75
+
76
+ if (startSide !== endSide) {
77
+ return {
78
+ context: {
79
+ ...base,
80
+ text: '',
81
+ location:
82
+ `lines ${range.start} (${sideName(startSide)}) through ` +
83
+ `${range.end} (${sideName(endSide)}) of the current git diff`,
84
+ note: trans.__('diff selection')
85
+ },
86
+ anchor
87
+ };
88
+ }
89
+
90
+ const startLine = Math.min(range.start, range.end);
91
+ const endLine = Math.max(range.start, range.end);
92
+ const isOld = startSide === 'deletions';
93
+ const source = isOld ? model.reference.source : model.challenger.source;
94
+ const ref = describeSource(source, isOld ? 'old' : 'new');
95
+ return {
96
+ context: {
97
+ ...base,
98
+ startLine,
99
+ endLine,
100
+ // Only new-side lines of a working-tree diff index the file on disk;
101
+ // old-side and historical line numbers must not be used to highlight
102
+ // the working file.
103
+ linesInWorkingFile:
104
+ !isOld && model.challenger.source === Git.Diff.SpecialRef.WORKING,
105
+ text: sliceLines(isOld ? oldText : newText, startLine, endLine),
106
+ location: `the ${sideName(startSide)} (${ref}) of the current git diff`,
107
+ // New-side lines read as the file's current content, so only old-side
108
+ // ranges need a clarifying tag in the popup header.
109
+ ...(isOld ? { note: trans.__('old version') } : {})
110
+ },
111
+ anchor
112
+ };
113
+ }
@@ -2,10 +2,12 @@ import { JupyterFrontEnd } from '@jupyterlab/application';
2
2
  import { IThemeManager, MainAreaWidget } from '@jupyterlab/apputils';
3
3
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
4
  import { Contents } from '@jupyterlab/services';
5
+ import type { TranslationBundle } from '@jupyterlab/translation';
5
6
  import { ToolbarButton, launchIcon } from '@jupyterlab/ui-components';
6
7
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
7
8
  import { ISignal, Signal } from '@lumino/signaling';
8
9
 
10
+ import type { IAskAgent } from '../askAgent/tokens';
9
11
  import { getTreeIcon } from '../fileBrowser/icons';
10
12
  import { fileChangeToDiffModel } from './diffModel';
11
13
  import {
@@ -45,9 +47,10 @@ export function pinnedDiffWidgetId(change: IFileChange): string {
45
47
  return `xtralab:diff:pinned:${change.group}:${change.path}`;
46
48
  }
47
49
 
48
- function formatTitle(change: IFileChange): string {
50
+ function formatTitle(change: IFileChange, trans: TranslationBundle): string {
49
51
  const name = change.path.split('/').pop() ?? change.path;
50
- const groupLabel = change.group === 'staged' ? 'Staged' : 'Working';
52
+ const groupLabel =
53
+ change.group === 'staged' ? trans.__('Staged') : trans.__('Working');
51
54
  return `${name} (${groupLabel})`;
52
55
  }
53
56
 
@@ -58,11 +61,13 @@ export class DiffMainAreaWidget extends MainAreaWidget<XtralabDiffWidget> {
58
61
  constructor(
59
62
  options: MainAreaWidget.IOptions<XtralabDiffWidget>,
60
63
  change: IFileChange,
61
- pinned: boolean
64
+ pinned: boolean,
65
+ trans: TranslationBundle
62
66
  ) {
63
67
  super(options);
64
68
  this._change = change;
65
69
  this._pinned = pinned;
70
+ this._trans = trans;
66
71
  }
67
72
 
68
73
  get change(): IFileChange {
@@ -88,13 +93,14 @@ export class DiffMainAreaWidget extends MainAreaWidget<XtralabDiffWidget> {
88
93
  setChange(repoPath: string, change: IFileChange): void {
89
94
  this._change = change;
90
95
  this.content.setModel(fileChangeToDiffModel(repoPath, change));
91
- this.title.label = formatTitle(change);
96
+ this.title.label = formatTitle(change, this._trans);
92
97
  this.title.caption = change.path;
93
98
  this.title.icon = getTreeIcon(change.path);
94
99
  }
95
100
 
96
101
  private _change: IFileChange;
97
102
  private _pinned: boolean;
103
+ private _trans: TranslationBundle;
98
104
  private _pinnedChanged = new Signal<this, boolean>(this);
99
105
  }
100
106
 
@@ -104,6 +110,8 @@ interface ICreateDiffWidgetOptions {
104
110
  themeManager: IThemeManager | null;
105
111
  contentsManager: Contents.IManager;
106
112
  rendermime: IRenderMimeRegistry | null;
113
+ askAgent: IAskAgent | null;
114
+ trans: TranslationBundle;
107
115
  pinned?: boolean;
108
116
  onPinned?: (widget: DiffMainAreaWidget) => void;
109
117
  }
@@ -114,16 +122,23 @@ interface ICreateDiffWidgetOptions {
114
122
  function createDiffWidget(
115
123
  options: ICreateDiffWidgetOptions
116
124
  ): DiffMainAreaWidget {
117
- const { repoPath, change, themeManager, contentsManager, rendermime } =
118
- options;
125
+ const {
126
+ repoPath,
127
+ change,
128
+ themeManager,
129
+ contentsManager,
130
+ rendermime,
131
+ askAgent,
132
+ trans
133
+ } = options;
119
134
  const pinned = options.pinned === true;
120
135
  const content = new XtralabDiffWidget(
121
136
  fileChangeToDiffModel(repoPath, change),
122
- { contentsManager, rendermime, themeManager }
137
+ { contentsManager, rendermime, themeManager, askAgent, trans }
123
138
  );
124
- const widget = new DiffMainAreaWidget({ content }, change, pinned);
139
+ const widget = new DiffMainAreaWidget({ content }, change, pinned, trans);
125
140
  widget.id = pinned ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
126
- widget.title.label = formatTitle(change);
141
+ widget.title.label = formatTitle(change, trans);
127
142
  widget.title.caption = change.path;
128
143
  widget.title.closable = true;
129
144
  widget.title.icon = getTreeIcon(change.path);
@@ -134,7 +149,7 @@ function createDiffWidget(
134
149
 
135
150
  const pinButton = new ToolbarButton({
136
151
  icon: launchIcon,
137
- tooltip: 'Pin tab',
152
+ tooltip: trans.__('Pin tab'),
138
153
  onClick: () => widget.pin()
139
154
  });
140
155
  if (widget.pinned) {
@@ -155,7 +170,7 @@ function createDiffWidget(
155
170
  }
156
171
  };
157
172
  const onPinnedChanged = (
158
- _sender: DiffMainAreaWidget,
173
+ sender: DiffMainAreaWidget,
159
174
  value: boolean
160
175
  ): void => {
161
176
  if (widget.isDisposed) {
@@ -191,7 +206,7 @@ function createDiffWidget(
191
206
  return widget;
192
207
  }
193
208
 
194
- export interface IRegisterGitCommandsOptions {
209
+ interface IRegisterGitCommandsOptions {
195
210
  app: JupyterFrontEnd;
196
211
  themeManager: IThemeManager | null;
197
212
  /**
@@ -202,6 +217,14 @@ export interface IRegisterGitCommandsOptions {
202
217
  * Rendermime registry used by notebook diffs; may be `null`.
203
218
  */
204
219
  rendermime: IRenderMimeRegistry | null;
220
+ /**
221
+ * Ask-agent popup for prompting an agent about diff lines; may be `null`.
222
+ */
223
+ askAgent: IAskAgent | null;
224
+ /**
225
+ * Translation bundle for user-facing strings.
226
+ */
227
+ trans: TranslationBundle;
205
228
  /**
206
229
  * Track newly created diff widgets before they are added to the shell.
207
230
  */
@@ -225,6 +248,8 @@ export function registerGitCommands(
225
248
  themeManager,
226
249
  contentsManager,
227
250
  rendermime,
251
+ askAgent,
252
+ trans,
228
253
  trackDiff,
229
254
  findDiff,
230
255
  onPinned
@@ -232,8 +257,8 @@ export function registerGitCommands(
232
257
  const { commands } = app;
233
258
 
234
259
  commands.addCommand(CommandIDs.openDiff, {
235
- label: 'Open Git Diff',
236
- caption: 'Open a side-by-side diff for a changed file',
260
+ label: trans.__('Open Git Diff'),
261
+ caption: trans.__('Open a side-by-side diff for a changed file'),
237
262
  execute: async (args: ReadonlyPartialJSONObject) => {
238
263
  const typed = args as unknown as CommandArguments.IOpenDiff;
239
264
  if (typed?.change === undefined) {
@@ -254,6 +279,8 @@ export function registerGitCommands(
254
279
  themeManager,
255
280
  contentsManager,
256
281
  rendermime,
282
+ askAgent,
283
+ trans,
257
284
  pinned: pin,
258
285
  onPinned
259
286
  });
@@ -9,7 +9,7 @@ import type { GitReference, IFileChange } from './tokens';
9
9
  * Resolve the old/new git refs for a launcher file change. Staged diffs are
10
10
  * INDEX vs HEAD; unstaged diffs are WORKING vs INDEX.
11
11
  */
12
- export function resolveReferences(change: IFileChange): {
12
+ function resolveReferences(change: IFileChange): {
13
13
  oldRef: GitReference | null;
14
14
  newRef: GitReference | null;
15
15
  } {