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.
- package/README.md +37 -229
- package/lib/agentSessions.d.ts +6 -2
- package/lib/askAgent/editorSelection.d.ts +53 -0
- package/lib/askAgent/editorSelection.js +95 -0
- package/lib/askAgent/icons.d.ts +7 -0
- package/lib/askAgent/icons.js +15 -0
- package/lib/askAgent/index.d.ts +32 -0
- package/lib/askAgent/index.js +741 -0
- package/lib/askAgent/popup.d.ts +83 -0
- package/lib/askAgent/popup.js +221 -0
- package/lib/askAgent/prompt.d.ts +24 -0
- package/lib/askAgent/prompt.js +116 -0
- package/lib/askAgent/queue.d.ts +93 -0
- package/lib/askAgent/queue.js +207 -0
- package/lib/askAgent/queuePanel.d.ts +76 -0
- package/lib/askAgent/queuePanel.js +188 -0
- package/lib/askAgent/targetPicker.d.ts +58 -0
- package/lib/askAgent/targetPicker.js +74 -0
- package/lib/askAgent/tokens.d.ts +124 -0
- package/lib/askAgent/tokens.js +13 -0
- package/lib/commandBar/index.d.ts +12 -8
- package/lib/commandBar/index.js +58 -41
- package/lib/editorBreadcrumbs/index.js +9 -4
- package/lib/editorBreadcrumbs/widget.d.ts +7 -7
- package/lib/editorBreadcrumbs/widget.js +6 -8
- package/lib/fileBrowser/commands.d.ts +2 -8
- package/lib/fileBrowser/commands.js +39 -30
- package/lib/fileBrowser/contents.d.ts +8 -3
- package/lib/fileBrowser/fileBrowser.d.ts +2 -1
- package/lib/fileBrowser/widget.d.ts +5 -9
- package/lib/fileBrowser/widget.js +1 -1
- package/lib/fileTypeIcons/index.js +5 -2
- package/lib/git/api.d.ts +0 -26
- package/lib/git/api.js +2 -44
- package/lib/git/askRequest.d.ts +23 -0
- package/lib/git/askRequest.js +90 -0
- package/lib/git/commands.d.ts +14 -2
- package/lib/git/commands.js +16 -13
- package/lib/git/diffModel.d.ts +1 -9
- package/lib/git/diffModel.js +1 -1
- package/lib/git/diffProvider.js +8 -3
- package/lib/git/diffSurface.d.ts +19 -2
- package/lib/git/diffSurface.js +64 -18
- package/lib/git/diffTheme.d.ts +0 -4
- package/lib/git/diffTheme.js +1 -1
- package/lib/git/diffWidget.d.ts +9 -0
- package/lib/git/diffWidget.js +38 -10
- package/lib/git/imageDiff.d.ts +17 -4
- package/lib/git/imageDiff.js +90 -30
- package/lib/git/index.js +7 -2
- package/lib/git/notebookDiff.d.ts +10 -67
- package/lib/git/notebookDiff.js +62 -69
- package/lib/git/tokens.d.ts +5 -2
- package/lib/highlight/index.js +12 -8
- package/lib/index.js +2 -0
- package/lib/launcher/agents.d.ts +11 -1
- package/lib/launcher/agents.js +26 -8
- package/lib/launcher/commands.d.ts +6 -2
- package/lib/launcher/dashboard.d.ts +8 -1
- package/lib/launcher/dashboard.js +39 -33
- package/lib/launcher/editorRegistry.js +5 -3
- package/lib/launcher/editors.d.ts +24 -7
- package/lib/launcher/editors.js +14 -5
- package/lib/launcher/icons.d.ts +0 -37
- package/lib/launcher/icons.js +11 -12
- package/lib/launcher/index.js +5 -2
- package/lib/launcher/invocation.d.ts +0 -15
- package/lib/launcher/invocation.js +1 -1
- package/lib/launcher/schemaDefaults.d.ts +3 -1
- package/lib/launcher/schemaDefaults.js +3 -1
- package/lib/mimeData.d.ts +7 -0
- package/lib/mimeData.js +16 -0
- package/lib/omnibox/index.d.ts +4 -0
- package/lib/omnibox/index.js +43 -2
- package/lib/omnibox/model.d.ts +43 -14
- package/lib/omnibox/model.js +117 -14
- package/lib/omnibox/recents.d.ts +65 -0
- package/lib/omnibox/recents.js +109 -0
- package/lib/omnibox/tokens.d.ts +6 -2
- package/lib/omnibox/widget.d.ts +23 -6
- package/lib/omnibox/widget.js +26 -7
- package/lib/showOutput/index.js +15 -18
- package/lib/terminals/agentTerminals.d.ts +63 -0
- package/lib/terminals/agentTerminals.js +157 -0
- package/lib/terminals/index.d.ts +7 -1
- package/lib/terminals/index.js +50 -30
- package/lib/terminals/model.d.ts +11 -2
- package/lib/terminals/model.js +27 -8
- package/lib/terminals/tokens.d.ts +66 -0
- package/lib/terminals/tokens.js +7 -0
- package/lib/terminals/widget.d.ts +3 -6
- package/lib/terminals/widget.js +1 -1
- package/lib/topBar/index.js +3 -3
- package/lib/walkthrough/index.js +14 -3
- package/lib/walkthrough/panel.d.ts +12 -4
- package/lib/walkthrough/panel.js +9 -3
- package/package.json +49 -51
- package/schema/command-bar.json +10 -0
- package/schema/launcher.json +4 -4
- package/schema/omnibox.json +15 -0
- package/src/agentSessions.ts +6 -2
- package/src/askAgent/editorSelection.ts +138 -0
- package/src/askAgent/icons.ts +16 -0
- package/src/askAgent/index.ts +915 -0
- package/src/askAgent/popup.tsx +407 -0
- package/src/askAgent/prompt.ts +144 -0
- package/src/askAgent/queue.ts +269 -0
- package/src/askAgent/queuePanel.tsx +418 -0
- package/src/askAgent/targetPicker.tsx +209 -0
- package/src/askAgent/tokens.ts +139 -0
- package/src/commandBar/index.ts +75 -49
- package/src/editorBreadcrumbs/index.ts +14 -4
- package/src/editorBreadcrumbs/widget.ts +11 -9
- package/src/fileBrowser/commands.ts +45 -31
- package/src/fileBrowser/contents.ts +7 -3
- package/src/fileBrowser/fileBrowser.tsx +1 -1
- package/src/fileBrowser/widget.tsx +5 -3
- package/src/fileTypeIcons/index.ts +6 -3
- package/src/git/api.ts +2 -61
- package/src/git/askRequest.ts +113 -0
- package/src/git/commands.ts +41 -14
- package/src/git/diffModel.ts +1 -1
- package/src/git/diffProvider.tsx +11 -3
- package/src/git/diffSurface.tsx +114 -22
- package/src/git/diffTheme.ts +1 -1
- package/src/git/diffWidget.tsx +54 -7
- package/src/git/imageDiff.tsx +169 -58
- package/src/git/index.ts +10 -2
- package/src/git/notebookDiff.tsx +91 -57
- package/src/git/tokens.ts +4 -2
- package/src/highlight/index.ts +12 -8
- package/src/index.ts +2 -0
- package/src/launcher/agents.ts +31 -7
- package/src/launcher/commands.ts +6 -2
- package/src/launcher/dashboard.tsx +86 -38
- package/src/launcher/editorRegistry.ts +10 -4
- package/src/launcher/editors.ts +34 -9
- package/src/launcher/icons.ts +11 -12
- package/src/launcher/index.ts +5 -2
- package/src/launcher/invocation.ts +1 -1
- package/src/launcher/schemaDefaults.ts +3 -1
- package/src/mimeData.ts +20 -0
- package/src/omnibox/index.ts +48 -1
- package/src/omnibox/model.ts +173 -33
- package/src/omnibox/recents.ts +155 -0
- package/src/omnibox/tokens.ts +6 -2
- package/src/omnibox/widget.tsx +54 -13
- package/src/showOutput/index.ts +20 -23
- package/src/terminals/agentTerminals.ts +238 -0
- package/src/terminals/index.ts +54 -30
- package/src/terminals/model.ts +28 -7
- package/src/terminals/tokens.ts +77 -0
- package/src/terminals/widget.tsx +4 -2
- package/src/topBar/index.ts +12 -6
- package/src/walkthrough/index.ts +19 -4
- package/src/walkthrough/panel.ts +15 -5
- package/style/askAgent.css +544 -0
- package/style/commandBar.css +12 -33
- package/style/git.css +72 -31
- package/style/index.css +1 -0
- package/style/index.js +2 -0
- 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
|
-
|
|
62
|
+
const OPEN_WITH_SUBMENU_ID = 'jp-contextmenu-xtralab-open-with';
|
|
63
63
|
|
|
64
|
-
|
|
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
|
-
/**
|
|
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) ??
|
|
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:
|
|
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({
|
|
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(
|
|
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
|
-
|
|
34
|
-
/**
|
|
33
|
+
interface IListedDirectory {
|
|
34
|
+
/**
|
|
35
|
+
* Canonical paths for every immediate child of the requested directory.
|
|
36
|
+
*/
|
|
35
37
|
paths: string[];
|
|
36
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/src/git/commands.ts
CHANGED
|
@@ -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 =
|
|
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 {
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/git/diffModel.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
+
function resolveReferences(change: IFileChange): {
|
|
13
13
|
oldRef: GitReference | null;
|
|
14
14
|
newRef: GitReference | null;
|
|
15
15
|
} {
|