jupyterlab_ai_code_assistants_extension 1.2.5 → 1.2.18
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 +1 -1
- package/lib/core/icons.d.ts +1 -0
- package/lib/core/icons.js +14 -0
- package/lib/core/panel.d.ts +6 -0
- package/lib/core/panel.js +45 -25
- package/lib/core/popup.d.ts +5 -0
- package/lib/core/popup.js +11 -8
- package/lib/core/terminals.d.ts +7 -1
- package/lib/core/terminals.js +7 -3
- package/lib/index.js +23 -3
- package/package.json +2 -1
- package/src/core/icons.ts +16 -0
- package/src/core/panel.ts +51 -20
- package/src/core/popup.ts +19 -8
- package/src/core/terminals.ts +18 -3
- package/src/index.ts +28 -3
- package/style/base.css +56 -7
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
31
31
|
- **Three-section side panel** - Favorites, Recent, and All projects, each scrolling independently
|
|
32
32
|
- **One-click resume** - click a row to jump back into that session in a terminal; an open terminal for the project is reused instead of duplicated
|
|
33
33
|
- **Launcher tiles** - every enabled assistant has a tile in an "AI Assistants" section of the JupyterLab Launcher (after Other, under the extension's own robot-head icon); a click opens the assistant in the file browser's current folder, resuming the folder's conversation when one exists and starting a new one otherwise, through the companion `jupyterlab_basic_terminal_extension` (installed automatically). Disable an assistant and its tile goes; disable them all and the section goes with them
|
|
34
|
-
- **Your terminal's `claude -c` follows the panel** (Claude) - switching to
|
|
34
|
+
- **Your terminal's `claude -c` follows the panel** (Claude) - switching to a conversation makes plain `claude -c` in that project resume that same conversation, including one that has been compacted, which Claude Code otherwise refuses to continue. Launching repairs a compacted conversation the same way, but only switching moves what `claude -c` picks
|
|
35
35
|
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id with last-activity time; "Manage Sessions..." opens a searchable popup over the full list with multi-select delete and per-row open and copy-id buttons
|
|
36
36
|
- **Branch session** - fork the current conversation into a new named session via the right-click menu; each assistant forks its own way (Claude's native `--fork-session`, Codex's `codex fork`, server-side copies for Kimi and Gemini) behind the same menu item
|
|
37
37
|
- **Launch modes under each assistant's own name** - Claude's skip-permissions, Codex's approval bypass, Kimi's `--yolo`, Gemini's YOLO; unsafe variants carry a shield glyph in the launch menus
|
package/lib/core/icons.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare const addIcon: LabIcon;
|
|
|
9
9
|
export declare const branchIcon: LabIcon;
|
|
10
10
|
export declare const switchIcon: LabIcon;
|
|
11
11
|
export declare const filterIcon: LabIcon;
|
|
12
|
+
export declare const statusDotIcon: LabIcon;
|
|
12
13
|
/** Build (or reuse) the LabIcon for a provider's own glyph. */
|
|
13
14
|
export declare function providerIcon(name: string, svgstr: string): LabIcon;
|
|
14
15
|
/**
|
package/lib/core/icons.js
CHANGED
|
@@ -103,6 +103,20 @@ export const filterIcon = new LabIcon({
|
|
|
103
103
|
name: `${ICON_PREFIX}:filter`,
|
|
104
104
|
svgstr: filterSvgStr
|
|
105
105
|
});
|
|
106
|
+
// Row status mark - one filled circle, drawn for a conversation that is live
|
|
107
|
+
// or under remote control. An SVG rather than a rounded span so its colour is
|
|
108
|
+
// a CSS `fill` on the circle itself, the way every other glyph here is
|
|
109
|
+
// recoloured through its class (DEF-PANE-174). The circle carries no `fill`
|
|
110
|
+
// attribute: the stylesheet is the single source of that colour, and a
|
|
111
|
+
// presentation attribute here would be a second one to keep in step
|
|
112
|
+
// (DEF-PANE-179).
|
|
113
|
+
const statusDotSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
|
114
|
+
<circle class="jp-AiAssistantsPanel-dotGlyph" cx="8" cy="8" r="8"/>
|
|
115
|
+
</svg>`;
|
|
116
|
+
export const statusDotIcon = new LabIcon({
|
|
117
|
+
name: `${ICON_PREFIX}:status-dot`,
|
|
118
|
+
svgstr: statusDotSvgStr
|
|
119
|
+
});
|
|
106
120
|
// Provider glyphs already built, by icon name. LabIcon warns and returns the
|
|
107
121
|
// first registration for a duplicate name, so a provider re-registered by a
|
|
108
122
|
// live settings toggle must resolve to the icon it built the first time
|
package/lib/core/panel.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
|
2
2
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
3
3
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
4
4
|
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
5
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
5
6
|
import { Message } from '@lumino/messaging';
|
|
6
7
|
import { Widget } from '@lumino/widgets';
|
|
7
8
|
import { IProviderDescriptor, IProviderHooks } from './types';
|
|
@@ -303,6 +304,7 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
303
304
|
private _stopPolling;
|
|
304
305
|
private readonly _app;
|
|
305
306
|
private readonly _descriptor;
|
|
307
|
+
private readonly _trans;
|
|
306
308
|
private readonly _hooks;
|
|
307
309
|
private readonly _serverSettings;
|
|
308
310
|
private _rootDir;
|
|
@@ -366,5 +368,9 @@ export declare namespace AssistantSessionsPanel {
|
|
|
366
368
|
terminalTracker?: ITerminalTracker | null;
|
|
367
369
|
fileBrowser?: IDefaultFileBrowser | null;
|
|
368
370
|
colourfulTabs?: IColourfulTabs | null;
|
|
371
|
+
/** Bundle every user-visible string passes through. Optional so a
|
|
372
|
+
* caller that does not translate keeps working - the null bundle
|
|
373
|
+
* returns each string unchanged. */
|
|
374
|
+
trans?: TranslationBundle;
|
|
369
375
|
}
|
|
370
376
|
}
|
package/lib/core/panel.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Clipboard, Dialog, InputDialog, Notification, showDialog } from '@jupyterlab/apputils';
|
|
2
2
|
import { MenuSvg, closeIcon, folderIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
3
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
3
4
|
import { CommandRegistry } from '@lumino/commands';
|
|
4
5
|
import { UUID } from '@lumino/coreutils';
|
|
5
6
|
import { Widget } from '@lumino/widgets';
|
|
6
7
|
import { ColourStore } from './colour';
|
|
7
|
-
import { addIcon, branchIcon, cleanupIcon, filterIcon, providerIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon, switchIcon } from './icons';
|
|
8
|
+
import { addIcon, branchIcon, cleanupIcon, filterIcon, providerIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon, statusDotIcon, switchIcon } from './icons';
|
|
8
9
|
import { DEFAULT_RECENT_LIMIT, MAX_RECENT_LIMIT, MIN_RECENT_LIMIT } from './limits';
|
|
9
10
|
import { resolveLaunchMode, resolvedLaunchModeEntry } from './modes';
|
|
10
11
|
import { showManageSessionsPopup } from './popup';
|
|
@@ -97,7 +98,7 @@ function saveExpanded(storageKey, state) {
|
|
|
97
98
|
*/
|
|
98
99
|
export class AssistantSessionsPanel extends Widget {
|
|
99
100
|
constructor(options) {
|
|
100
|
-
var _a, _b, _c, _d;
|
|
101
|
+
var _a, _b, _c, _d, _e;
|
|
101
102
|
super();
|
|
102
103
|
this._rootDir = '';
|
|
103
104
|
this._deleteToTrash = true;
|
|
@@ -138,9 +139,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
138
139
|
this._app = options.app;
|
|
139
140
|
this._descriptor = options.descriptor;
|
|
140
141
|
this._hooks = (_a = options.hooks) !== null && _a !== void 0 ? _a : {};
|
|
142
|
+
this._trans =
|
|
143
|
+
(_b = options.trans) !== null && _b !== void 0 ? _b : nullTranslator.load('jupyterlab_ai_code_assistants_extension');
|
|
141
144
|
this._serverSettings = options.app.serviceManager.serverSettings;
|
|
142
145
|
this.setRoot(options.rootDir, options.deleteToTrash !== false);
|
|
143
|
-
this._fileBrowser = (
|
|
146
|
+
this._fileBrowser = (_c = options.fileBrowser) !== null && _c !== void 0 ? _c : null;
|
|
144
147
|
this._expandedKey = `jupyterlab_ai_code_assistants_extension:${this._descriptor.id}:expanded`;
|
|
145
148
|
this._expanded = loadExpanded(this._expandedKey);
|
|
146
149
|
this._colours = new ColourStore(this._descriptor.id, this._serverSettings);
|
|
@@ -148,9 +151,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
148
151
|
app: options.app,
|
|
149
152
|
descriptor: this._descriptor,
|
|
150
153
|
colourStore: this._colours,
|
|
151
|
-
terminalTracker: (
|
|
152
|
-
colourfulTabs: (
|
|
153
|
-
serverSettings: this._serverSettings
|
|
154
|
+
terminalTracker: (_d = options.terminalTracker) !== null && _d !== void 0 ? _d : null,
|
|
155
|
+
colourfulTabs: (_e = options.colourfulTabs) !== null && _e !== void 0 ? _e : null,
|
|
156
|
+
serverSettings: this._serverSettings,
|
|
157
|
+
trans: this._trans
|
|
154
158
|
});
|
|
155
159
|
this.id = panelWidgetId(this._descriptor.id);
|
|
156
160
|
this.title.icon = providerIcon(this._descriptor.iconName, this._descriptor.iconSvg);
|
|
@@ -299,7 +303,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
299
303
|
header.appendChild(newBtn);
|
|
300
304
|
const filterBtn = document.createElement('button');
|
|
301
305
|
filterBtn.className = 'jp-AiAssistantsPanel-iconButton';
|
|
302
|
-
filterBtn.title = 'Filter sessions';
|
|
306
|
+
filterBtn.title = this._trans.__('Filter sessions');
|
|
303
307
|
// The pressed state is drawn with a class, which says nothing to a screen
|
|
304
308
|
// reader; the filter bar starts hidden, so this starts false.
|
|
305
309
|
filterBtn.setAttribute('aria-pressed', 'false');
|
|
@@ -309,7 +313,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
309
313
|
this._filterBtn = filterBtn;
|
|
310
314
|
const refreshBtn = document.createElement('button');
|
|
311
315
|
refreshBtn.className = 'jp-AiAssistantsPanel-iconButton';
|
|
312
|
-
refreshBtn.title = 'Refresh';
|
|
316
|
+
refreshBtn.title = this._trans.__('Refresh');
|
|
313
317
|
refreshIcon.element({ container: refreshBtn });
|
|
314
318
|
refreshBtn.addEventListener('click', () => this.refresh());
|
|
315
319
|
header.appendChild(refreshBtn);
|
|
@@ -327,7 +331,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
327
331
|
search.type = 'search';
|
|
328
332
|
search.className = 'jp-AiAssistantsPanel-search';
|
|
329
333
|
search.placeholder = 'Filter sessions...';
|
|
330
|
-
search.setAttribute('aria-label', 'Filter sessions');
|
|
334
|
+
search.setAttribute('aria-label', this._trans.__('Filter sessions'));
|
|
331
335
|
search.spellcheck = false;
|
|
332
336
|
search.addEventListener('input', () => {
|
|
333
337
|
this._filter = search.value;
|
|
@@ -337,7 +341,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
337
341
|
this._searchEl = search;
|
|
338
342
|
const searchClear = document.createElement('button');
|
|
339
343
|
searchClear.className = 'jp-AiAssistantsPanel-searchClear';
|
|
340
|
-
searchClear.title = 'Clear filter';
|
|
344
|
+
searchClear.title = this._trans.__('Clear filter');
|
|
341
345
|
searchClear.hidden = true;
|
|
342
346
|
closeIcon.element({ container: searchClear });
|
|
343
347
|
searchClear.addEventListener('click', () => {
|
|
@@ -593,7 +597,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
593
597
|
body: `Remove "${name}" from ${this._descriptor.label}? This drops the ` +
|
|
594
598
|
`entire project history and every conversation it holds - ` +
|
|
595
599
|
`${this._disposalVerb}. This cannot be undone.`,
|
|
596
|
-
buttons: [
|
|
600
|
+
buttons: [
|
|
601
|
+
Dialog.cancelButton(),
|
|
602
|
+
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
603
|
+
],
|
|
597
604
|
// Cancel, explicitly. JupyterLab defaults `defaultButton` to the LAST
|
|
598
605
|
// button and focuses it on attach, so omitting this handed the keyboard
|
|
599
606
|
// to Remove - one Enter on a dialog that has just appeared and the
|
|
@@ -627,11 +634,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
627
634
|
const extra = session.extra_sessions;
|
|
628
635
|
const name = this._lookupName(session);
|
|
629
636
|
const confirm = await showDialog({
|
|
630
|
-
title: 'Clean Up Parallel Sessions',
|
|
637
|
+
title: this._trans.__('Clean Up Parallel Sessions'),
|
|
631
638
|
body: `Remove ${extra} parallel session${extra === 1 ? '' : 's'} from ` +
|
|
632
639
|
`"${name}"? The current conversation is kept; the rest are ` +
|
|
633
640
|
`${this._disposalVerb}. This cannot be undone.`,
|
|
634
|
-
buttons: [
|
|
641
|
+
buttons: [
|
|
642
|
+
Dialog.cancelButton(),
|
|
643
|
+
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
644
|
+
],
|
|
635
645
|
// Cancel, as above (DEF-51).
|
|
636
646
|
defaultButton: 0
|
|
637
647
|
});
|
|
@@ -651,9 +661,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
651
661
|
bar.max = 1;
|
|
652
662
|
body.node.appendChild(bar);
|
|
653
663
|
const dialog = new Dialog({
|
|
654
|
-
title: 'Clean Up Parallel Sessions',
|
|
664
|
+
title: this._trans.__('Clean Up Parallel Sessions'),
|
|
655
665
|
body,
|
|
656
|
-
buttons: [Dialog.okButton({ label: 'Close' })]
|
|
666
|
+
buttons: [Dialog.okButton({ label: this._trans.__('Close') })]
|
|
657
667
|
});
|
|
658
668
|
// Close stays live while the work runs. Hiding it took away the only exit
|
|
659
669
|
// a keyboard user has: the dialog listens for Escape on its own node, it
|
|
@@ -1481,7 +1491,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1481
1491
|
row.classList.add('jp-mod-busy');
|
|
1482
1492
|
const spinner = document.createElement('span');
|
|
1483
1493
|
spinner.className = 'jp-AiAssistantsPanel-spinner';
|
|
1484
|
-
spinner.title = 'Removing...';
|
|
1494
|
+
spinner.title = this._trans.__('Removing...');
|
|
1485
1495
|
row.appendChild(spinner);
|
|
1486
1496
|
}
|
|
1487
1497
|
else {
|
|
@@ -1531,7 +1541,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1531
1541
|
// menu, rather than becoming a second hit target inside the row.
|
|
1532
1542
|
const star = document.createElement('span');
|
|
1533
1543
|
star.className = 'jp-AiAssistantsPanel-favStar';
|
|
1534
|
-
star.setAttribute('aria-label', 'Favorite');
|
|
1544
|
+
star.setAttribute('aria-label', this._trans.__('Favorite'));
|
|
1535
1545
|
star.setAttribute('role', 'img');
|
|
1536
1546
|
starFilledIcon.element({ container: star });
|
|
1537
1547
|
row.appendChild(star);
|
|
@@ -1625,12 +1635,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1625
1635
|
// words go on an aria-label.
|
|
1626
1636
|
if (this._descriptor.hasRemoteControl && session.remote_control) {
|
|
1627
1637
|
dot.className = 'jp-AiAssistantsPanel-dot';
|
|
1628
|
-
|
|
1638
|
+
statusDotIcon.element({ container: dot });
|
|
1639
|
+
dot.title = this._trans.__('Remote control session is active');
|
|
1629
1640
|
dot.setAttribute('aria-label', dot.title);
|
|
1630
1641
|
return dot;
|
|
1631
1642
|
}
|
|
1632
1643
|
if (this._descriptor.hasLiveProcess && session.live) {
|
|
1633
1644
|
dot.className = 'jp-AiAssistantsPanel-dot';
|
|
1645
|
+
statusDotIcon.element({ container: dot });
|
|
1634
1646
|
dot.title = `A ${this._descriptor.label} session is active in this project`;
|
|
1635
1647
|
dot.setAttribute('aria-label', dot.title);
|
|
1636
1648
|
return dot;
|
|
@@ -1836,6 +1848,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1836
1848
|
var _a;
|
|
1837
1849
|
return ((_a = this._resolvedVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? shieldIcon : undefined;
|
|
1838
1850
|
}
|
|
1851
|
+
/* eslint-disable jupyter/command-described-by --
|
|
1852
|
+
These commands go on the private CommandRegistry built below, which only
|
|
1853
|
+
this panel's own row context menu reads. `describedBy` is metadata for
|
|
1854
|
+
the application registry's consumers - the palette, settings, other
|
|
1855
|
+
extensions - and none of them can see or invoke a command registered
|
|
1856
|
+
here. */
|
|
1839
1857
|
_setupCommands() {
|
|
1840
1858
|
this._commands = new CommandRegistry();
|
|
1841
1859
|
const active = () => this._activeSession;
|
|
@@ -1899,7 +1917,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1899
1917
|
});
|
|
1900
1918
|
}
|
|
1901
1919
|
this._commands.addCommand(this._cmd('open-terminal'), {
|
|
1902
|
-
label: 'Open Terminal',
|
|
1920
|
+
label: this._trans.__('Open Terminal'),
|
|
1903
1921
|
icon: terminalIcon,
|
|
1904
1922
|
execute: () => {
|
|
1905
1923
|
const s = active();
|
|
@@ -1924,7 +1942,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1924
1942
|
}
|
|
1925
1943
|
});
|
|
1926
1944
|
this._commands.addCommand(this._cmd('show-in-filebrowser'), {
|
|
1927
|
-
label: 'Show in File Browser',
|
|
1945
|
+
label: this._trans.__('Show in File Browser'),
|
|
1928
1946
|
icon: folderIcon,
|
|
1929
1947
|
execute: () => {
|
|
1930
1948
|
const s = active();
|
|
@@ -1946,7 +1964,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1946
1964
|
}
|
|
1947
1965
|
});
|
|
1948
1966
|
this._commands.addCommand(this._cmd('copy-path'), {
|
|
1949
|
-
label: 'Copy Path',
|
|
1967
|
+
label: this._trans.__('Copy Path'),
|
|
1950
1968
|
execute: () => {
|
|
1951
1969
|
const s = active();
|
|
1952
1970
|
if (s) {
|
|
@@ -1955,7 +1973,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1955
1973
|
}
|
|
1956
1974
|
});
|
|
1957
1975
|
this._commands.addCommand(this._cmd('copy-session-id'), {
|
|
1958
|
-
label: 'Copy Session ID',
|
|
1976
|
+
label: this._trans.__('Copy Session ID'),
|
|
1959
1977
|
execute: () => {
|
|
1960
1978
|
var _a;
|
|
1961
1979
|
const id = (_a = active()) === null || _a === void 0 ? void 0 : _a.session_id;
|
|
@@ -2087,6 +2105,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2087
2105
|
});
|
|
2088
2106
|
this._buildMenus();
|
|
2089
2107
|
}
|
|
2108
|
+
/* eslint-enable jupyter/command-described-by */
|
|
2090
2109
|
/** How many forced launch variants would actually render right now. Zero
|
|
2091
2110
|
* means every variant builds the launch the plain item already builds, so a
|
|
2092
2111
|
* menu offering both is a menu with one entry. */
|
|
@@ -2106,17 +2125,17 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2106
2125
|
// Lists the project's other conversations. Items are rebuilt on every
|
|
2107
2126
|
// context-menu open from a fresh branches fetch.
|
|
2108
2127
|
this._switchSubmenu = this._menu();
|
|
2109
|
-
this._switchSubmenu.title.label = 'Switch and Manage Sessions';
|
|
2128
|
+
this._switchSubmenu.title.label = this._trans.__('Switch and Manage Sessions');
|
|
2110
2129
|
this._switchSubmenu.title.icon = switchIcon;
|
|
2111
2130
|
// Opens a conversation directly in its own terminal, as opposed to the
|
|
2112
2131
|
// switch submenu which only changes which one the row points at. Several
|
|
2113
2132
|
// conversations can be open at once, independently.
|
|
2114
2133
|
this._openBranchSubmenu = this._menu();
|
|
2115
|
-
this._openBranchSubmenu.title.label = 'Open Branched Conversation';
|
|
2134
|
+
this._openBranchSubmenu.title.label = this._trans.__('Open Branched Conversation');
|
|
2116
2135
|
this._openBranchSubmenu.title.icon = terminalIcon;
|
|
2117
2136
|
// Groups the branch launch modes.
|
|
2118
2137
|
this._branchSessionMenu = this._menu();
|
|
2119
|
-
this._branchSessionMenu.title.label = 'Branch Session';
|
|
2138
|
+
this._branchSessionMenu.title.label = this._trans.__('Branch Session');
|
|
2120
2139
|
this._branchSessionMenu.title.icon = branchIcon;
|
|
2121
2140
|
this._branchSessionMenu.addItem({ command: this._cmd('branch-session') });
|
|
2122
2141
|
for (const mode of this._variantModes) {
|
|
@@ -2333,6 +2352,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2333
2352
|
return;
|
|
2334
2353
|
}
|
|
2335
2354
|
showManageSessionsPopup({
|
|
2355
|
+
trans: this._trans,
|
|
2336
2356
|
branches: this._lastBranches,
|
|
2337
2357
|
current: this._lastBranchesCurrent || session.session_id,
|
|
2338
2358
|
projectName: this._lookupName(session),
|
package/lib/core/popup.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
1
2
|
import { IBranch } from './types';
|
|
2
3
|
/**
|
|
3
4
|
* The Manage Sessions popup: a searchable, scrollable table over every
|
|
@@ -9,6 +10,10 @@ import { IBranch } from './types';
|
|
|
9
10
|
*/
|
|
10
11
|
export declare namespace ManageSessionsPopup {
|
|
11
12
|
interface IOptions {
|
|
13
|
+
/** Bundle every user-visible string passes through. Optional so a
|
|
14
|
+
* caller that does not translate keeps working - the null bundle
|
|
15
|
+
* returns each string unchanged. */
|
|
16
|
+
trans?: TranslationBundle;
|
|
12
17
|
/** Conversations other than the current one. */
|
|
13
18
|
branches: IBranch[];
|
|
14
19
|
/** The project's current conversation id, pinned at the top. */
|
package/lib/core/popup.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Clipboard, Dialog } from '@jupyterlab/apputils';
|
|
2
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
2
3
|
import { copyIcon } from '@jupyterlab/ui-components';
|
|
3
4
|
import { Widget } from '@lumino/widgets';
|
|
4
5
|
import { branchIcon } from './icons';
|
|
5
6
|
/** Compact copy button for a popup row. `stopPropagation` keeps the click from
|
|
6
7
|
* switching or selecting the row it sits in. */
|
|
7
|
-
function copyButton(sessionId) {
|
|
8
|
+
function copyButton(sessionId, trans) {
|
|
8
9
|
const btn = document.createElement('button');
|
|
9
10
|
btn.type = 'button';
|
|
10
11
|
btn.className = 'jp-AiAssistantsPanel-branchCopy';
|
|
11
|
-
btn.title = 'Copy session id';
|
|
12
|
+
btn.title = trans.__('Copy session id');
|
|
12
13
|
copyIcon.element({ container: btn });
|
|
13
14
|
btn.addEventListener('click', e => {
|
|
14
15
|
e.stopPropagation();
|
|
@@ -17,6 +18,8 @@ function copyButton(sessionId) {
|
|
|
17
18
|
return btn;
|
|
18
19
|
}
|
|
19
20
|
export function showManageSessionsPopup(options) {
|
|
21
|
+
var _a;
|
|
22
|
+
const trans = (_a = options.trans) !== null && _a !== void 0 ? _a : nullTranslator.load('jupyterlab_ai_code_assistants_extension');
|
|
20
23
|
// Local working copy so deletions can refresh the list in place.
|
|
21
24
|
let items = [...options.branches];
|
|
22
25
|
const selected = new Set();
|
|
@@ -31,7 +34,7 @@ export function showManageSessionsPopup(options) {
|
|
|
31
34
|
const search = document.createElement('input');
|
|
32
35
|
search.type = 'search';
|
|
33
36
|
search.placeholder = 'Filter sessions...';
|
|
34
|
-
search.setAttribute('aria-label', 'Filter sessions');
|
|
37
|
+
search.setAttribute('aria-label', trans.__('Filter sessions'));
|
|
35
38
|
search.className = 'jp-AiAssistantsPanel-branchSearch';
|
|
36
39
|
body.appendChild(search);
|
|
37
40
|
// Header strip: select-all left, conversation count right.
|
|
@@ -52,7 +55,7 @@ export function showManageSessionsPopup(options) {
|
|
|
52
55
|
list.className = 'jp-AiAssistantsPanel-branchList';
|
|
53
56
|
// role=group makes the aria-label apply to the conversation list region.
|
|
54
57
|
list.setAttribute('role', 'group');
|
|
55
|
-
list.setAttribute('aria-label', 'Conversations');
|
|
58
|
+
list.setAttribute('aria-label', trans.__('Conversations'));
|
|
56
59
|
body.appendChild(list);
|
|
57
60
|
const footer = document.createElement('div');
|
|
58
61
|
footer.className = 'jp-AiAssistantsPanel-branchFooter';
|
|
@@ -78,12 +81,12 @@ export function showManageSessionsPopup(options) {
|
|
|
78
81
|
body.appendChild(srLive);
|
|
79
82
|
const bodyWidget = new Widget({ node: body });
|
|
80
83
|
const dialog = new Dialog({
|
|
81
|
-
title: 'Manage Sessions',
|
|
84
|
+
title: trans.__('Manage Sessions'),
|
|
82
85
|
body: bodyWidget,
|
|
83
86
|
// `Close`, not `Cancel`: deletions here are committed as they happen, so
|
|
84
87
|
// the only exit must not read as an offer to undo them. The cleanup dialog
|
|
85
88
|
// in the panel already says Close for the same reason.
|
|
86
|
-
buttons: [Dialog.okButton({ label: 'Close' })]
|
|
89
|
+
buttons: [Dialog.okButton({ label: trans.__('Close') })]
|
|
87
90
|
});
|
|
88
91
|
// Per-row "Open" launches that conversation in its own terminal and closes
|
|
89
92
|
// the popup. stopPropagation keeps the click from toggling selection or
|
|
@@ -163,7 +166,7 @@ export function showManageSessionsPopup(options) {
|
|
|
163
166
|
badge.textContent = 'current';
|
|
164
167
|
currentRow.appendChild(badge);
|
|
165
168
|
currentRow.appendChild(openButton(options.current));
|
|
166
|
-
currentRow.appendChild(copyButton(options.current));
|
|
169
|
+
currentRow.appendChild(copyButton(options.current, trans));
|
|
167
170
|
list.appendChild(currentRow);
|
|
168
171
|
const matches = visibleMatches();
|
|
169
172
|
if (matches.length === 0) {
|
|
@@ -241,7 +244,7 @@ export function showManageSessionsPopup(options) {
|
|
|
241
244
|
time.textContent = options.formatTime(b.file_mtime);
|
|
242
245
|
row.appendChild(time);
|
|
243
246
|
row.appendChild(openButton(b.session_id));
|
|
244
|
-
row.appendChild(copyButton(b.session_id));
|
|
247
|
+
row.appendChild(copyButton(b.session_id, trans));
|
|
245
248
|
const activate = () => {
|
|
246
249
|
// Selection mode: while anything is ticked, row clicks toggle
|
|
247
250
|
// selection - no accidental switch mid-selection.
|
package/lib/core/terminals.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
2
|
import { Dialog } from '@jupyterlab/apputils';
|
|
3
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
3
4
|
import { ServerConnection } from '@jupyterlab/services';
|
|
4
5
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
5
6
|
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
@@ -11,7 +12,7 @@ import { ILaunchRequest, IProviderDescriptor, ISession, ITerminalProbeResponse }
|
|
|
11
12
|
* Hide does not abort the request - it only takes the app-wide scrim away, so
|
|
12
13
|
* a stalled launch cannot leave the user staring at a modal with no exit. If
|
|
13
14
|
* the launch later succeeds the terminal simply appears. */
|
|
14
|
-
export declare function showLaunchSpinner(title: string): Dialog<unknown>;
|
|
15
|
+
export declare function showLaunchSpinner(title: string, trans: TranslationBundle): Dialog<unknown>;
|
|
15
16
|
/**
|
|
16
17
|
* Terminals for one provider: the reuse ladder, the launch call and the tab
|
|
17
18
|
* tint pass.
|
|
@@ -202,6 +203,7 @@ export declare class TerminalManager {
|
|
|
202
203
|
private _key;
|
|
203
204
|
private readonly _app;
|
|
204
205
|
private readonly _descriptor;
|
|
206
|
+
private readonly _trans;
|
|
205
207
|
private readonly _colours;
|
|
206
208
|
private readonly _tracker;
|
|
207
209
|
/** The injected colour token narrowed to its ownership API, or null when the
|
|
@@ -227,5 +229,9 @@ export declare namespace TerminalManager {
|
|
|
227
229
|
terminalTracker: ITerminalTracker | null;
|
|
228
230
|
colourfulTabs: IColourfulTabs | null;
|
|
229
231
|
serverSettings: ServerConnection.ISettings;
|
|
232
|
+
/** Bundle every user-visible string passes through. Optional so a
|
|
233
|
+
* caller that does not translate keeps working - the null bundle
|
|
234
|
+
* returns each string unchanged. */
|
|
235
|
+
trans?: TranslationBundle;
|
|
230
236
|
}
|
|
231
237
|
}
|
package/lib/core/terminals.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Dialog, Notification } from '@jupyterlab/apputils';
|
|
2
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
2
3
|
import { Widget } from '@lumino/widgets';
|
|
3
4
|
import { effectiveColour, sessionForCwds } from './colour';
|
|
4
5
|
import { LOG_PREFIX } from './registry';
|
|
@@ -51,7 +52,7 @@ function warnLegacyCompanion() {
|
|
|
51
52
|
* Hide does not abort the request - it only takes the app-wide scrim away, so
|
|
52
53
|
* a stalled launch cannot leave the user staring at a modal with no exit. If
|
|
53
54
|
* the launch later succeeds the terminal simply appears. */
|
|
54
|
-
export function showLaunchSpinner(title) {
|
|
55
|
+
export function showLaunchSpinner(title, trans) {
|
|
55
56
|
const body = new Widget();
|
|
56
57
|
body.node.className = 'jp-AiAssistantsPanel-launchOverlay';
|
|
57
58
|
const spinner = document.createElement('div');
|
|
@@ -65,7 +66,7 @@ export function showLaunchSpinner(title) {
|
|
|
65
66
|
// the request is already on the wire and the terminal will still open and
|
|
66
67
|
// take focus. A button named Cancel that does not cancel is worse than no
|
|
67
68
|
// button.
|
|
68
|
-
buttons: [Dialog.okButton({ label: 'Hide' })]
|
|
69
|
+
buttons: [Dialog.okButton({ label: trans.__('Hide') })]
|
|
69
70
|
});
|
|
70
71
|
// `launch()` returns a promise nobody awaits - disposing an open Lumino
|
|
71
72
|
// dialog rejects it with `undefined`, so swallow that here and keep a benign
|
|
@@ -84,6 +85,7 @@ export function showLaunchSpinner(title) {
|
|
|
84
85
|
*/
|
|
85
86
|
export class TerminalManager {
|
|
86
87
|
constructor(options) {
|
|
88
|
+
var _a;
|
|
87
89
|
// One claim per terminal, keyed by widget id - see `_claimTab` for why that
|
|
88
90
|
// id identifies exactly one widget.
|
|
89
91
|
this._claims = new Map();
|
|
@@ -115,6 +117,8 @@ export class TerminalManager {
|
|
|
115
117
|
this._colours = options.colourStore;
|
|
116
118
|
this._tracker = options.terminalTracker;
|
|
117
119
|
this._serverSettings = options.serverSettings;
|
|
120
|
+
this._trans =
|
|
121
|
+
(_a = options.trans) !== null && _a !== void 0 ? _a : nullTranslator.load('jupyterlab_ai_code_assistants_extension');
|
|
118
122
|
// This narrowing decides whether the extension tints at all. Against a
|
|
119
123
|
// companion with no ownership API it paints nothing: a colour the user
|
|
120
124
|
// picks on a tab cannot be captured there, so the next pass would repaint
|
|
@@ -209,7 +213,7 @@ export class TerminalManager {
|
|
|
209
213
|
* that conversation now belongs to reuses this very terminal.
|
|
210
214
|
*/
|
|
211
215
|
async launch(request, tagSessionId) {
|
|
212
|
-
const spinner = showLaunchSpinner(`Opening ${this._descriptor.label}
|
|
216
|
+
const spinner = showLaunchSpinner(`Opening ${this._descriptor.label}`, this._trans);
|
|
213
217
|
try {
|
|
214
218
|
const launched = await requestProvider(this._descriptor.id, 'launch', this._serverSettings, { method: 'POST', body: JSON.stringify(request) });
|
|
215
219
|
const widget = await this._app.commands.execute('terminal:open', {
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
|
4
4
|
import { ILauncher } from '@jupyterlab/launcher';
|
|
5
5
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
6
6
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
7
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
7
8
|
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
8
9
|
import { launcherTileIcon, providerIcon } from './core/icons';
|
|
9
10
|
import { DEFAULT_RECENT_LIMIT } from './core/limits';
|
|
@@ -38,10 +39,15 @@ const plugin = {
|
|
|
38
39
|
ITerminalTracker,
|
|
39
40
|
IDefaultFileBrowser,
|
|
40
41
|
IColourfulTabs,
|
|
41
|
-
ILauncher
|
|
42
|
+
ILauncher,
|
|
43
|
+
ITranslator
|
|
42
44
|
],
|
|
43
|
-
activate: async (app, labShell, restorer, settingRegistry, terminalTracker, fileBrowser, colourfulTabs, launcher) => {
|
|
45
|
+
activate: async (app, labShell, restorer, settingRegistry, terminalTracker, fileBrowser, colourfulTabs, launcher, translator) => {
|
|
44
46
|
console.log('JupyterLab extension jupyterlab_ai_code_assistants_extension is activated!');
|
|
47
|
+
// Every user-visible string in the panels goes through this bundle.
|
|
48
|
+
// `ITranslator` is optional, and the null translator returns each string
|
|
49
|
+
// unchanged, so an install without one behaves exactly as before.
|
|
50
|
+
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab_ai_code_assistants_extension');
|
|
45
51
|
// A duplicate id throws here, at registration, rather than surfacing later
|
|
46
52
|
// as two panels quietly fighting over one widget id.
|
|
47
53
|
const registry = new ProviderRegistry(PROVIDERS);
|
|
@@ -163,7 +169,8 @@ const plugin = {
|
|
|
163
169
|
deleteToTrash: status ? status.delete_to_trash !== false : false,
|
|
164
170
|
terminalTracker,
|
|
165
171
|
fileBrowser,
|
|
166
|
-
colourfulTabs
|
|
172
|
+
colourfulTabs,
|
|
173
|
+
trans
|
|
167
174
|
});
|
|
168
175
|
applySharedSettings(panel);
|
|
169
176
|
dock(panel);
|
|
@@ -172,6 +179,8 @@ const plugin = {
|
|
|
172
179
|
restorer === null || restorer === void 0 ? void 0 : restorer.add(panel, panelWidgetId(id));
|
|
173
180
|
const command = app.commands.addCommand(commandId(id, 'refresh'), {
|
|
174
181
|
label: `Refresh ${module.descriptor.panelTitle}`,
|
|
182
|
+
// Takes no arguments, which is what `null` says here.
|
|
183
|
+
describedBy: { args: null },
|
|
175
184
|
execute: () => panel.refresh()
|
|
176
185
|
});
|
|
177
186
|
// The Launcher tile's command. It is registered whether or not a
|
|
@@ -184,6 +193,17 @@ const plugin = {
|
|
|
184
193
|
// The section header borrows this icon too; the view draws the
|
|
185
194
|
// joint icon there and the provider's own on the tile.
|
|
186
195
|
icon: launcherTileIcon(providerIcon(module.descriptor.iconName, module.descriptor.iconSvg)),
|
|
196
|
+
describedBy: {
|
|
197
|
+
args: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
cwd: {
|
|
201
|
+
type: 'string',
|
|
202
|
+
description: 'Folder to start the assistant in. The server root when absent.'
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
},
|
|
187
207
|
// The Launcher puts the file browser's folder in `cwd` on every
|
|
188
208
|
// click; the panel resolves it against the server root.
|
|
189
209
|
execute: args => panel.launchHere(args.cwd)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_ai_code_assistants_extension",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.18",
|
|
4
4
|
"description": "Jupyterlab extension to replace individual jupyterlab_*_code_extension and integrate into one, that offers joint capability, joint settings page and individual right toolbar panels per code assistant, with the ability in settings to decide which code assistant support is on (default - all are enabled)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"@jupyterlab/services": "^7.0.0",
|
|
67
67
|
"@jupyterlab/settingregistry": "^4.0.0",
|
|
68
68
|
"@jupyterlab/terminal": "^4.0.0",
|
|
69
|
+
"@jupyterlab/translation": "^4.0.0",
|
|
69
70
|
"@jupyterlab/ui-components": "^4.0.0",
|
|
70
71
|
"@lumino/commands": "^2.0.0",
|
|
71
72
|
"@lumino/coreutils": "^2.0.0",
|
package/src/core/icons.ts
CHANGED
|
@@ -126,6 +126,22 @@ export const filterIcon = new LabIcon({
|
|
|
126
126
|
svgstr: filterSvgStr
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
// Row status mark - one filled circle, drawn for a conversation that is live
|
|
130
|
+
// or under remote control. An SVG rather than a rounded span so its colour is
|
|
131
|
+
// a CSS `fill` on the circle itself, the way every other glyph here is
|
|
132
|
+
// recoloured through its class (DEF-PANE-174). The circle carries no `fill`
|
|
133
|
+
// attribute: the stylesheet is the single source of that colour, and a
|
|
134
|
+
// presentation attribute here would be a second one to keep in step
|
|
135
|
+
// (DEF-PANE-179).
|
|
136
|
+
const statusDotSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
|
137
|
+
<circle class="jp-AiAssistantsPanel-dotGlyph" cx="8" cy="8" r="8"/>
|
|
138
|
+
</svg>`;
|
|
139
|
+
|
|
140
|
+
export const statusDotIcon = new LabIcon({
|
|
141
|
+
name: `${ICON_PREFIX}:status-dot`,
|
|
142
|
+
svgstr: statusDotSvgStr
|
|
143
|
+
});
|
|
144
|
+
|
|
129
145
|
// Provider glyphs already built, by icon name. LabIcon warns and returns the
|
|
130
146
|
// first registration for a duplicate name, so a provider re-registered by a
|
|
131
147
|
// live settings toggle must resolve to the icon it built the first time
|
package/src/core/panel.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
folderIcon,
|
|
18
18
|
terminalIcon
|
|
19
19
|
} from '@jupyterlab/ui-components';
|
|
20
|
+
import { TranslationBundle, nullTranslator } from '@jupyterlab/translation';
|
|
20
21
|
import { CommandRegistry } from '@lumino/commands';
|
|
21
22
|
import { UUID } from '@lumino/coreutils';
|
|
22
23
|
import { Message } from '@lumino/messaging';
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
removeIcon,
|
|
34
35
|
shieldIcon,
|
|
35
36
|
starFilledIcon,
|
|
37
|
+
statusDotIcon,
|
|
36
38
|
switchIcon
|
|
37
39
|
} from './icons';
|
|
38
40
|
import {
|
|
@@ -176,6 +178,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
176
178
|
this._app = options.app;
|
|
177
179
|
this._descriptor = options.descriptor;
|
|
178
180
|
this._hooks = options.hooks ?? {};
|
|
181
|
+
this._trans =
|
|
182
|
+
options.trans ??
|
|
183
|
+
nullTranslator.load('jupyterlab_ai_code_assistants_extension');
|
|
179
184
|
this._serverSettings = options.app.serviceManager.serverSettings;
|
|
180
185
|
this.setRoot(options.rootDir, options.deleteToTrash !== false);
|
|
181
186
|
this._fileBrowser = options.fileBrowser ?? null;
|
|
@@ -189,7 +194,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
189
194
|
colourStore: this._colours,
|
|
190
195
|
terminalTracker: options.terminalTracker ?? null,
|
|
191
196
|
colourfulTabs: options.colourfulTabs ?? null,
|
|
192
|
-
serverSettings: this._serverSettings
|
|
197
|
+
serverSettings: this._serverSettings,
|
|
198
|
+
trans: this._trans
|
|
193
199
|
});
|
|
194
200
|
|
|
195
201
|
this.id = panelWidgetId(this._descriptor.id);
|
|
@@ -363,7 +369,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
363
369
|
|
|
364
370
|
const filterBtn = document.createElement('button');
|
|
365
371
|
filterBtn.className = 'jp-AiAssistantsPanel-iconButton';
|
|
366
|
-
filterBtn.title = 'Filter sessions';
|
|
372
|
+
filterBtn.title = this._trans.__('Filter sessions');
|
|
367
373
|
// The pressed state is drawn with a class, which says nothing to a screen
|
|
368
374
|
// reader; the filter bar starts hidden, so this starts false.
|
|
369
375
|
filterBtn.setAttribute('aria-pressed', 'false');
|
|
@@ -374,7 +380,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
374
380
|
|
|
375
381
|
const refreshBtn = document.createElement('button');
|
|
376
382
|
refreshBtn.className = 'jp-AiAssistantsPanel-iconButton';
|
|
377
|
-
refreshBtn.title = 'Refresh';
|
|
383
|
+
refreshBtn.title = this._trans.__('Refresh');
|
|
378
384
|
refreshIcon.element({ container: refreshBtn });
|
|
379
385
|
refreshBtn.addEventListener('click', () => this.refresh());
|
|
380
386
|
header.appendChild(refreshBtn);
|
|
@@ -394,7 +400,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
394
400
|
search.type = 'search';
|
|
395
401
|
search.className = 'jp-AiAssistantsPanel-search';
|
|
396
402
|
search.placeholder = 'Filter sessions...';
|
|
397
|
-
search.setAttribute('aria-label', 'Filter sessions');
|
|
403
|
+
search.setAttribute('aria-label', this._trans.__('Filter sessions'));
|
|
398
404
|
search.spellcheck = false;
|
|
399
405
|
search.addEventListener('input', () => {
|
|
400
406
|
this._filter = search.value;
|
|
@@ -405,7 +411,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
405
411
|
|
|
406
412
|
const searchClear = document.createElement('button');
|
|
407
413
|
searchClear.className = 'jp-AiAssistantsPanel-searchClear';
|
|
408
|
-
searchClear.title = 'Clear filter';
|
|
414
|
+
searchClear.title = this._trans.__('Clear filter');
|
|
409
415
|
searchClear.hidden = true;
|
|
410
416
|
closeIcon.element({ container: searchClear });
|
|
411
417
|
searchClear.addEventListener('click', () => {
|
|
@@ -696,7 +702,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
696
702
|
`Remove "${name}" from ${this._descriptor.label}? This drops the ` +
|
|
697
703
|
`entire project history and every conversation it holds - ` +
|
|
698
704
|
`${this._disposalVerb}. This cannot be undone.`,
|
|
699
|
-
buttons: [
|
|
705
|
+
buttons: [
|
|
706
|
+
Dialog.cancelButton(),
|
|
707
|
+
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
708
|
+
],
|
|
700
709
|
// Cancel, explicitly. JupyterLab defaults `defaultButton` to the LAST
|
|
701
710
|
// button and focuses it on attach, so omitting this handed the keyboard
|
|
702
711
|
// to Remove - one Enter on a dialog that has just appeared and the
|
|
@@ -736,12 +745,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
736
745
|
const extra = session.extra_sessions;
|
|
737
746
|
const name = this._lookupName(session);
|
|
738
747
|
const confirm = await showDialog({
|
|
739
|
-
title: 'Clean Up Parallel Sessions',
|
|
748
|
+
title: this._trans.__('Clean Up Parallel Sessions'),
|
|
740
749
|
body:
|
|
741
750
|
`Remove ${extra} parallel session${extra === 1 ? '' : 's'} from ` +
|
|
742
751
|
`"${name}"? The current conversation is kept; the rest are ` +
|
|
743
752
|
`${this._disposalVerb}. This cannot be undone.`,
|
|
744
|
-
buttons: [
|
|
753
|
+
buttons: [
|
|
754
|
+
Dialog.cancelButton(),
|
|
755
|
+
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
756
|
+
],
|
|
745
757
|
// Cancel, as above (DEF-51).
|
|
746
758
|
defaultButton: 0
|
|
747
759
|
});
|
|
@@ -767,9 +779,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
767
779
|
body.node.appendChild(bar);
|
|
768
780
|
|
|
769
781
|
const dialog = new Dialog<unknown>({
|
|
770
|
-
title: 'Clean Up Parallel Sessions',
|
|
782
|
+
title: this._trans.__('Clean Up Parallel Sessions'),
|
|
771
783
|
body,
|
|
772
|
-
buttons: [Dialog.okButton({ label: 'Close' })]
|
|
784
|
+
buttons: [Dialog.okButton({ label: this._trans.__('Close') })]
|
|
773
785
|
});
|
|
774
786
|
// Close stays live while the work runs. Hiding it took away the only exit
|
|
775
787
|
// a keyboard user has: the dialog listens for Escape on its own node, it
|
|
@@ -1770,7 +1782,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1770
1782
|
row.classList.add('jp-mod-busy');
|
|
1771
1783
|
const spinner = document.createElement('span');
|
|
1772
1784
|
spinner.className = 'jp-AiAssistantsPanel-spinner';
|
|
1773
|
-
spinner.title = 'Removing...';
|
|
1785
|
+
spinner.title = this._trans.__('Removing...');
|
|
1774
1786
|
row.appendChild(spinner);
|
|
1775
1787
|
} else {
|
|
1776
1788
|
row.appendChild(this._renderIndicator(session));
|
|
@@ -1823,7 +1835,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1823
1835
|
// menu, rather than becoming a second hit target inside the row.
|
|
1824
1836
|
const star = document.createElement('span');
|
|
1825
1837
|
star.className = 'jp-AiAssistantsPanel-favStar';
|
|
1826
|
-
star.setAttribute('aria-label', 'Favorite');
|
|
1838
|
+
star.setAttribute('aria-label', this._trans.__('Favorite'));
|
|
1827
1839
|
star.setAttribute('role', 'img');
|
|
1828
1840
|
starFilledIcon.element({ container: star });
|
|
1829
1841
|
row.appendChild(star);
|
|
@@ -1925,12 +1937,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1925
1937
|
// words go on an aria-label.
|
|
1926
1938
|
if (this._descriptor.hasRemoteControl && session.remote_control) {
|
|
1927
1939
|
dot.className = 'jp-AiAssistantsPanel-dot';
|
|
1928
|
-
|
|
1940
|
+
statusDotIcon.element({ container: dot });
|
|
1941
|
+
dot.title = this._trans.__('Remote control session is active');
|
|
1929
1942
|
dot.setAttribute('aria-label', dot.title);
|
|
1930
1943
|
return dot;
|
|
1931
1944
|
}
|
|
1932
1945
|
if (this._descriptor.hasLiveProcess && session.live) {
|
|
1933
1946
|
dot.className = 'jp-AiAssistantsPanel-dot';
|
|
1947
|
+
statusDotIcon.element({ container: dot });
|
|
1934
1948
|
dot.title = `A ${this._descriptor.label} session is active in this project`;
|
|
1935
1949
|
dot.setAttribute('aria-label', dot.title);
|
|
1936
1950
|
return dot;
|
|
@@ -2166,6 +2180,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2166
2180
|
return this._resolvedVariant()?.unsafe ? shieldIcon : undefined;
|
|
2167
2181
|
}
|
|
2168
2182
|
|
|
2183
|
+
/* eslint-disable jupyter/command-described-by --
|
|
2184
|
+
These commands go on the private CommandRegistry built below, which only
|
|
2185
|
+
this panel's own row context menu reads. `describedBy` is metadata for
|
|
2186
|
+
the application registry's consumers - the palette, settings, other
|
|
2187
|
+
extensions - and none of them can see or invoke a command registered
|
|
2188
|
+
here. */
|
|
2169
2189
|
private _setupCommands(): void {
|
|
2170
2190
|
this._commands = new CommandRegistry();
|
|
2171
2191
|
const active = (): ISession | null => this._activeSession;
|
|
@@ -2233,7 +2253,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2233
2253
|
}
|
|
2234
2254
|
|
|
2235
2255
|
this._commands.addCommand(this._cmd('open-terminal'), {
|
|
2236
|
-
label: 'Open Terminal',
|
|
2256
|
+
label: this._trans.__('Open Terminal'),
|
|
2237
2257
|
icon: terminalIcon,
|
|
2238
2258
|
execute: () => {
|
|
2239
2259
|
const s = active();
|
|
@@ -2267,7 +2287,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2267
2287
|
});
|
|
2268
2288
|
|
|
2269
2289
|
this._commands.addCommand(this._cmd('show-in-filebrowser'), {
|
|
2270
|
-
label: 'Show in File Browser',
|
|
2290
|
+
label: this._trans.__('Show in File Browser'),
|
|
2271
2291
|
icon: folderIcon,
|
|
2272
2292
|
execute: () => {
|
|
2273
2293
|
const s = active();
|
|
@@ -2298,7 +2318,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2298
2318
|
});
|
|
2299
2319
|
|
|
2300
2320
|
this._commands.addCommand(this._cmd('copy-path'), {
|
|
2301
|
-
label: 'Copy Path',
|
|
2321
|
+
label: this._trans.__('Copy Path'),
|
|
2302
2322
|
execute: () => {
|
|
2303
2323
|
const s = active();
|
|
2304
2324
|
if (s) {
|
|
@@ -2308,7 +2328,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2308
2328
|
});
|
|
2309
2329
|
|
|
2310
2330
|
this._commands.addCommand(this._cmd('copy-session-id'), {
|
|
2311
|
-
label: 'Copy Session ID',
|
|
2331
|
+
label: this._trans.__('Copy Session ID'),
|
|
2312
2332
|
execute: () => {
|
|
2313
2333
|
const id = active()?.session_id;
|
|
2314
2334
|
if (id) {
|
|
@@ -2448,6 +2468,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2448
2468
|
|
|
2449
2469
|
this._buildMenus();
|
|
2450
2470
|
}
|
|
2471
|
+
/* eslint-enable jupyter/command-described-by */
|
|
2451
2472
|
|
|
2452
2473
|
/** How many forced launch variants would actually render right now. Zero
|
|
2453
2474
|
* means every variant builds the launch the plain item already builds, so a
|
|
@@ -2470,19 +2491,23 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2470
2491
|
// Lists the project's other conversations. Items are rebuilt on every
|
|
2471
2492
|
// context-menu open from a fresh branches fetch.
|
|
2472
2493
|
this._switchSubmenu = this._menu();
|
|
2473
|
-
this._switchSubmenu.title.label =
|
|
2494
|
+
this._switchSubmenu.title.label = this._trans.__(
|
|
2495
|
+
'Switch and Manage Sessions'
|
|
2496
|
+
);
|
|
2474
2497
|
this._switchSubmenu.title.icon = switchIcon;
|
|
2475
2498
|
|
|
2476
2499
|
// Opens a conversation directly in its own terminal, as opposed to the
|
|
2477
2500
|
// switch submenu which only changes which one the row points at. Several
|
|
2478
2501
|
// conversations can be open at once, independently.
|
|
2479
2502
|
this._openBranchSubmenu = this._menu();
|
|
2480
|
-
this._openBranchSubmenu.title.label =
|
|
2503
|
+
this._openBranchSubmenu.title.label = this._trans.__(
|
|
2504
|
+
'Open Branched Conversation'
|
|
2505
|
+
);
|
|
2481
2506
|
this._openBranchSubmenu.title.icon = terminalIcon;
|
|
2482
2507
|
|
|
2483
2508
|
// Groups the branch launch modes.
|
|
2484
2509
|
this._branchSessionMenu = this._menu();
|
|
2485
|
-
this._branchSessionMenu.title.label = 'Branch Session';
|
|
2510
|
+
this._branchSessionMenu.title.label = this._trans.__('Branch Session');
|
|
2486
2511
|
this._branchSessionMenu.title.icon = branchIcon;
|
|
2487
2512
|
this._branchSessionMenu.addItem({ command: this._cmd('branch-session') });
|
|
2488
2513
|
for (const mode of this._variantModes) {
|
|
@@ -2719,6 +2744,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2719
2744
|
return;
|
|
2720
2745
|
}
|
|
2721
2746
|
showManageSessionsPopup({
|
|
2747
|
+
trans: this._trans,
|
|
2722
2748
|
branches: this._lastBranches,
|
|
2723
2749
|
current: this._lastBranchesCurrent || session.session_id,
|
|
2724
2750
|
projectName: this._lookupName(session),
|
|
@@ -2802,6 +2828,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2802
2828
|
|
|
2803
2829
|
private readonly _app: JupyterFrontEnd;
|
|
2804
2830
|
private readonly _descriptor: IProviderDescriptor;
|
|
2831
|
+
private readonly _trans: TranslationBundle;
|
|
2805
2832
|
private readonly _hooks: IProviderHooks;
|
|
2806
2833
|
private readonly _serverSettings: ServerConnection.ISettings;
|
|
2807
2834
|
private _rootDir = '';
|
|
@@ -2866,5 +2893,9 @@ export namespace AssistantSessionsPanel {
|
|
|
2866
2893
|
terminalTracker?: ITerminalTracker | null;
|
|
2867
2894
|
fileBrowser?: IDefaultFileBrowser | null;
|
|
2868
2895
|
colourfulTabs?: IColourfulTabs | null;
|
|
2896
|
+
/** Bundle every user-visible string passes through. Optional so a
|
|
2897
|
+
* caller that does not translate keeps working - the null bundle
|
|
2898
|
+
* returns each string unchanged. */
|
|
2899
|
+
trans?: TranslationBundle;
|
|
2869
2900
|
}
|
|
2870
2901
|
}
|
package/src/core/popup.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Clipboard, Dialog } from '@jupyterlab/apputils';
|
|
2
|
+
import { TranslationBundle, nullTranslator } from '@jupyterlab/translation';
|
|
2
3
|
import { copyIcon } from '@jupyterlab/ui-components';
|
|
3
4
|
import { Widget } from '@lumino/widgets';
|
|
4
5
|
|
|
@@ -15,6 +16,10 @@ import { IBranch } from './types';
|
|
|
15
16
|
*/
|
|
16
17
|
export namespace ManageSessionsPopup {
|
|
17
18
|
export interface IOptions {
|
|
19
|
+
/** Bundle every user-visible string passes through. Optional so a
|
|
20
|
+
* caller that does not translate keeps working - the null bundle
|
|
21
|
+
* returns each string unchanged. */
|
|
22
|
+
trans?: TranslationBundle;
|
|
18
23
|
/** Conversations other than the current one. */
|
|
19
24
|
branches: IBranch[];
|
|
20
25
|
/** The project's current conversation id, pinned at the top. */
|
|
@@ -50,11 +55,14 @@ export namespace ManageSessionsPopup {
|
|
|
50
55
|
|
|
51
56
|
/** Compact copy button for a popup row. `stopPropagation` keeps the click from
|
|
52
57
|
* switching or selecting the row it sits in. */
|
|
53
|
-
function copyButton(
|
|
58
|
+
function copyButton(
|
|
59
|
+
sessionId: string,
|
|
60
|
+
trans: TranslationBundle
|
|
61
|
+
): HTMLButtonElement {
|
|
54
62
|
const btn = document.createElement('button');
|
|
55
63
|
btn.type = 'button';
|
|
56
64
|
btn.className = 'jp-AiAssistantsPanel-branchCopy';
|
|
57
|
-
btn.title = 'Copy session id';
|
|
65
|
+
btn.title = trans.__('Copy session id');
|
|
58
66
|
copyIcon.element({ container: btn });
|
|
59
67
|
btn.addEventListener('click', e => {
|
|
60
68
|
e.stopPropagation();
|
|
@@ -66,6 +74,9 @@ function copyButton(sessionId: string): HTMLButtonElement {
|
|
|
66
74
|
export function showManageSessionsPopup(
|
|
67
75
|
options: ManageSessionsPopup.IOptions
|
|
68
76
|
): void {
|
|
77
|
+
const trans =
|
|
78
|
+
options.trans ??
|
|
79
|
+
nullTranslator.load('jupyterlab_ai_code_assistants_extension');
|
|
69
80
|
// Local working copy so deletions can refresh the list in place.
|
|
70
81
|
let items = [...options.branches];
|
|
71
82
|
const selected = new Set<string>();
|
|
@@ -82,7 +93,7 @@ export function showManageSessionsPopup(
|
|
|
82
93
|
const search = document.createElement('input');
|
|
83
94
|
search.type = 'search';
|
|
84
95
|
search.placeholder = 'Filter sessions...';
|
|
85
|
-
search.setAttribute('aria-label', 'Filter sessions');
|
|
96
|
+
search.setAttribute('aria-label', trans.__('Filter sessions'));
|
|
86
97
|
search.className = 'jp-AiAssistantsPanel-branchSearch';
|
|
87
98
|
body.appendChild(search);
|
|
88
99
|
|
|
@@ -105,7 +116,7 @@ export function showManageSessionsPopup(
|
|
|
105
116
|
list.className = 'jp-AiAssistantsPanel-branchList';
|
|
106
117
|
// role=group makes the aria-label apply to the conversation list region.
|
|
107
118
|
list.setAttribute('role', 'group');
|
|
108
|
-
list.setAttribute('aria-label', 'Conversations');
|
|
119
|
+
list.setAttribute('aria-label', trans.__('Conversations'));
|
|
109
120
|
body.appendChild(list);
|
|
110
121
|
|
|
111
122
|
const footer = document.createElement('div');
|
|
@@ -134,12 +145,12 @@ export function showManageSessionsPopup(
|
|
|
134
145
|
|
|
135
146
|
const bodyWidget = new Widget({ node: body });
|
|
136
147
|
const dialog = new Dialog({
|
|
137
|
-
title: 'Manage Sessions',
|
|
148
|
+
title: trans.__('Manage Sessions'),
|
|
138
149
|
body: bodyWidget,
|
|
139
150
|
// `Close`, not `Cancel`: deletions here are committed as they happen, so
|
|
140
151
|
// the only exit must not read as an offer to undo them. The cleanup dialog
|
|
141
152
|
// in the panel already says Close for the same reason.
|
|
142
|
-
buttons: [Dialog.okButton({ label: 'Close' })]
|
|
153
|
+
buttons: [Dialog.okButton({ label: trans.__('Close') })]
|
|
143
154
|
});
|
|
144
155
|
|
|
145
156
|
// Per-row "Open" launches that conversation in its own terminal and closes
|
|
@@ -232,7 +243,7 @@ export function showManageSessionsPopup(
|
|
|
232
243
|
badge.textContent = 'current';
|
|
233
244
|
currentRow.appendChild(badge);
|
|
234
245
|
currentRow.appendChild(openButton(options.current));
|
|
235
|
-
currentRow.appendChild(copyButton(options.current));
|
|
246
|
+
currentRow.appendChild(copyButton(options.current, trans));
|
|
236
247
|
list.appendChild(currentRow);
|
|
237
248
|
|
|
238
249
|
const matches = visibleMatches();
|
|
@@ -314,7 +325,7 @@ export function showManageSessionsPopup(
|
|
|
314
325
|
row.appendChild(time);
|
|
315
326
|
|
|
316
327
|
row.appendChild(openButton(b.session_id));
|
|
317
|
-
row.appendChild(copyButton(b.session_id));
|
|
328
|
+
row.appendChild(copyButton(b.session_id, trans));
|
|
318
329
|
|
|
319
330
|
const activate = (): void => {
|
|
320
331
|
// Selection mode: while anything is ticked, row clicks toggle
|
package/src/core/terminals.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
2
|
import { Dialog, Notification } from '@jupyterlab/apputils';
|
|
3
|
+
import { TranslationBundle, nullTranslator } from '@jupyterlab/translation';
|
|
3
4
|
import { ServerConnection } from '@jupyterlab/services';
|
|
4
5
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
5
6
|
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
@@ -76,7 +77,10 @@ function warnLegacyCompanion(): void {
|
|
|
76
77
|
* Hide does not abort the request - it only takes the app-wide scrim away, so
|
|
77
78
|
* a stalled launch cannot leave the user staring at a modal with no exit. If
|
|
78
79
|
* the launch later succeeds the terminal simply appears. */
|
|
79
|
-
export function showLaunchSpinner(
|
|
80
|
+
export function showLaunchSpinner(
|
|
81
|
+
title: string,
|
|
82
|
+
trans: TranslationBundle
|
|
83
|
+
): Dialog<unknown> {
|
|
80
84
|
const body = new Widget();
|
|
81
85
|
body.node.className = 'jp-AiAssistantsPanel-launchOverlay';
|
|
82
86
|
|
|
@@ -92,7 +96,7 @@ export function showLaunchSpinner(title: string): Dialog<unknown> {
|
|
|
92
96
|
// the request is already on the wire and the terminal will still open and
|
|
93
97
|
// take focus. A button named Cancel that does not cancel is worse than no
|
|
94
98
|
// button.
|
|
95
|
-
buttons: [Dialog.okButton({ label: 'Hide' })]
|
|
99
|
+
buttons: [Dialog.okButton({ label: trans.__('Hide') })]
|
|
96
100
|
});
|
|
97
101
|
// `launch()` returns a promise nobody awaits - disposing an open Lumino
|
|
98
102
|
// dialog rejects it with `undefined`, so swallow that here and keep a benign
|
|
@@ -117,6 +121,9 @@ export class TerminalManager {
|
|
|
117
121
|
this._colours = options.colourStore;
|
|
118
122
|
this._tracker = options.terminalTracker;
|
|
119
123
|
this._serverSettings = options.serverSettings;
|
|
124
|
+
this._trans =
|
|
125
|
+
options.trans ??
|
|
126
|
+
nullTranslator.load('jupyterlab_ai_code_assistants_extension');
|
|
120
127
|
|
|
121
128
|
// This narrowing decides whether the extension tints at all. Against a
|
|
122
129
|
// companion with no ownership API it paints nothing: a colour the user
|
|
@@ -226,7 +233,10 @@ export class TerminalManager {
|
|
|
226
233
|
* that conversation now belongs to reuses this very terminal.
|
|
227
234
|
*/
|
|
228
235
|
async launch(request: ILaunchRequest, tagSessionId?: string): Promise<any> {
|
|
229
|
-
const spinner = showLaunchSpinner(
|
|
236
|
+
const spinner = showLaunchSpinner(
|
|
237
|
+
`Opening ${this._descriptor.label}`,
|
|
238
|
+
this._trans
|
|
239
|
+
);
|
|
230
240
|
try {
|
|
231
241
|
const launched = await requestProvider<ILaunchResponse>(
|
|
232
242
|
this._descriptor.id,
|
|
@@ -821,6 +831,7 @@ export class TerminalManager {
|
|
|
821
831
|
|
|
822
832
|
private readonly _app: JupyterFrontEnd;
|
|
823
833
|
private readonly _descriptor: IProviderDescriptor;
|
|
834
|
+
private readonly _trans: TranslationBundle;
|
|
824
835
|
private readonly _colours: ColourStore;
|
|
825
836
|
private readonly _tracker: ITerminalTracker | null;
|
|
826
837
|
/** The injected colour token narrowed to its ownership API, or null when the
|
|
@@ -867,5 +878,9 @@ export namespace TerminalManager {
|
|
|
867
878
|
terminalTracker: ITerminalTracker | null;
|
|
868
879
|
colourfulTabs: IColourfulTabs | null;
|
|
869
880
|
serverSettings: ServerConnection.ISettings;
|
|
881
|
+
/** Bundle every user-visible string passes through. Optional so a
|
|
882
|
+
* caller that does not translate keeps working - the null bundle
|
|
883
|
+
* returns each string unchanged. */
|
|
884
|
+
trans?: TranslationBundle;
|
|
870
885
|
}
|
|
871
886
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
|
9
9
|
import { ILauncher } from '@jupyterlab/launcher';
|
|
10
10
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
11
11
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
12
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
12
13
|
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
13
14
|
import { IDisposable } from '@lumino/disposable';
|
|
14
15
|
|
|
@@ -73,7 +74,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
73
74
|
ITerminalTracker,
|
|
74
75
|
IDefaultFileBrowser,
|
|
75
76
|
IColourfulTabs,
|
|
76
|
-
ILauncher
|
|
77
|
+
ILauncher,
|
|
78
|
+
ITranslator
|
|
77
79
|
],
|
|
78
80
|
activate: async (
|
|
79
81
|
app: JupyterFrontEnd,
|
|
@@ -83,12 +85,20 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
83
85
|
terminalTracker: ITerminalTracker | null,
|
|
84
86
|
fileBrowser: IDefaultFileBrowser | null,
|
|
85
87
|
colourfulTabs: IColourfulTabs | null,
|
|
86
|
-
launcher: ILauncher | null
|
|
88
|
+
launcher: ILauncher | null,
|
|
89
|
+
translator: ITranslator | null
|
|
87
90
|
) => {
|
|
88
91
|
console.log(
|
|
89
92
|
'JupyterLab extension jupyterlab_ai_code_assistants_extension is activated!'
|
|
90
93
|
);
|
|
91
94
|
|
|
95
|
+
// Every user-visible string in the panels goes through this bundle.
|
|
96
|
+
// `ITranslator` is optional, and the null translator returns each string
|
|
97
|
+
// unchanged, so an install without one behaves exactly as before.
|
|
98
|
+
const trans = (translator ?? nullTranslator).load(
|
|
99
|
+
'jupyterlab_ai_code_assistants_extension'
|
|
100
|
+
);
|
|
101
|
+
|
|
92
102
|
// A duplicate id throws here, at registration, rather than surfacing later
|
|
93
103
|
// as two panels quietly fighting over one widget id.
|
|
94
104
|
const registry = new ProviderRegistry(PROVIDERS);
|
|
@@ -229,7 +239,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
229
239
|
deleteToTrash: status ? status.delete_to_trash !== false : false,
|
|
230
240
|
terminalTracker,
|
|
231
241
|
fileBrowser,
|
|
232
|
-
colourfulTabs
|
|
242
|
+
colourfulTabs,
|
|
243
|
+
trans
|
|
233
244
|
});
|
|
234
245
|
applySharedSettings(panel);
|
|
235
246
|
dock(panel);
|
|
@@ -238,6 +249,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
238
249
|
restorer?.add(panel, panelWidgetId(id));
|
|
239
250
|
const command = app.commands.addCommand(commandId(id, 'refresh'), {
|
|
240
251
|
label: `Refresh ${module.descriptor.panelTitle}`,
|
|
252
|
+
// Takes no arguments, which is what `null` says here.
|
|
253
|
+
describedBy: { args: null },
|
|
241
254
|
execute: () => panel.refresh()
|
|
242
255
|
});
|
|
243
256
|
// The Launcher tile's command. It is registered whether or not a
|
|
@@ -252,6 +265,18 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
252
265
|
icon: launcherTileIcon(
|
|
253
266
|
providerIcon(module.descriptor.iconName, module.descriptor.iconSvg)
|
|
254
267
|
),
|
|
268
|
+
describedBy: {
|
|
269
|
+
args: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
cwd: {
|
|
273
|
+
type: 'string',
|
|
274
|
+
description:
|
|
275
|
+
'Folder to start the assistant in. The server root when absent.'
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
},
|
|
255
280
|
// The Launcher puts the file browser's folder in `cwd` on every
|
|
256
281
|
// click; the panel resolves it against the server root.
|
|
257
282
|
execute: args => panel.launchHere(args.cwd as string | undefined)
|
package/style/base.css
CHANGED
|
@@ -349,17 +349,66 @@
|
|
|
349
349
|
flex: 0 0 auto;
|
|
350
350
|
width: 8px;
|
|
351
351
|
height: 8px;
|
|
352
|
-
border-radius: 50%;
|
|
353
352
|
display: inline-block;
|
|
354
353
|
}
|
|
355
354
|
|
|
356
355
|
.jp-AiAssistantsPanel-dot {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
356
|
+
display: inline-flex;
|
|
357
|
+
align-items: center;
|
|
358
|
+
|
|
359
|
+
/* drop-shadow, not box-shadow: the glyph is an svg now, and box-shadow would
|
|
360
|
+
halo its square element box rather than the circle inside it. */
|
|
361
|
+
filter: drop-shadow(0 0 2px var(--jp-success-color1));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.jp-AiAssistantsPanel-dot svg {
|
|
365
|
+
width: 8px;
|
|
366
|
+
height: 8px;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* The theme picks the green, through the circle's own fill rather than a
|
|
370
|
+
presentation attribute on the svg, the way every other glyph here is
|
|
371
|
+
recoloured (DEF-PANE-174). This dot is the ONLY graphic for "a process is
|
|
372
|
+
running here", so WCAG 1.4.11's 3:1 applies: color1 is md-green-500 on every
|
|
373
|
+
dark theme installed, 5.79:1 against the stock dark row. color0 clears the
|
|
374
|
+
bar too and was used before, but it is a step darker again and at 8px reads
|
|
375
|
+
as a near-black mark rather than a green light, which is the whole of what
|
|
376
|
+
the dot has to say. The light themes need a different answer entirely - see
|
|
377
|
+
the rule below. */
|
|
378
|
+
.jp-AiAssistantsPanel-dotGlyph {
|
|
379
|
+
fill: var(--jp-success-color1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* Light themes take a different dot, because both halves of the one above stop
|
|
383
|
+
working there. JupyterLab stamps the theme's own `isLight` onto the body, so
|
|
384
|
+
the switch is CSS and no TypeScript sees it.
|
|
385
|
+
|
|
386
|
+
Fill - `--jp-success-color1` is a dark green on every light theme installed
|
|
387
|
+
(md-green-700 on JupyterLab Light, #27632a on the others), which is the
|
|
388
|
+
near-black mark the rule above rejects (DEF-PANE-175). md-green-500 is the
|
|
389
|
+
green the dark themes already carry, defined on `:root` by JupyterLab's own
|
|
390
|
+
materialcolors.css, so no fallback copy belongs here.
|
|
391
|
+
|
|
392
|
+
Glow - a mint ring, md-green-200, three passes at 2px. The dot's own green
|
|
393
|
+
would separate nothing from the dot, and white was tried and rejected on
|
|
394
|
+
sight (DEF-PANE-201): on the mid-grey rows the installed light themes draw
|
|
395
|
+
(#bec3ca, #c0c0c0) a white ring reads as a bright blob, not a light. A pale
|
|
396
|
+
green three steps lighter than the fill reads as the fill's own glow instead.
|
|
397
|
+
Three passes because drop-shadow spreads its alpha over the blur, so one
|
|
398
|
+
pass stays faint; 2px because at 1px the ring is barely wider than the
|
|
399
|
+
circle's anti-aliasing and at 3px stacked deeper it washes over the first
|
|
400
|
+
letter of the name. Size and shade chosen from rendered candidates on the
|
|
401
|
+
live row (2026-09-07). Stated plainly: no pale halo reaches WCAG 1.4.11's
|
|
402
|
+
3:1 on those rows - pure white against #bec3ca is 1.77:1, the ceiling - and
|
|
403
|
+
the dark contour that would was rejected on sight (DEF-PANE-185). */
|
|
404
|
+
body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dot {
|
|
405
|
+
filter: drop-shadow(0 0 2px var(--md-green-200))
|
|
406
|
+
drop-shadow(0 0 2px var(--md-green-200))
|
|
407
|
+
drop-shadow(0 0 2px var(--md-green-200));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotGlyph {
|
|
411
|
+
fill: var(--md-green-500);
|
|
363
412
|
}
|
|
364
413
|
|
|
365
414
|
.jp-AiAssistantsPanel-dotPlaceholder {
|