jupyterlab_ai_code_assistants_extension 1.2.7 → 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.js +6 -3
- package/lib/core/panel.d.ts +6 -0
- package/lib/core/panel.js +42 -24
- 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 +6 -3
- package/src/core/panel.ts +48 -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 +41 -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.js
CHANGED
|
@@ -105,10 +105,13 @@ export const filterIcon = new LabIcon({
|
|
|
105
105
|
});
|
|
106
106
|
// Row status mark - one filled circle, drawn for a conversation that is live
|
|
107
107
|
// or under remote control. An SVG rather than a rounded span so its colour is
|
|
108
|
-
// a CSS `fill` on the circle itself
|
|
109
|
-
//
|
|
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).
|
|
110
113
|
const statusDotSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
|
111
|
-
<circle class="jp-AiAssistantsPanel-dotGlyph" cx="8" cy="8" r="8"
|
|
114
|
+
<circle class="jp-AiAssistantsPanel-dotGlyph" cx="8" cy="8" r="8"/>
|
|
112
115
|
</svg>`;
|
|
113
116
|
export const statusDotIcon = new LabIcon({
|
|
114
117
|
name: `${ICON_PREFIX}:status-dot`,
|
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,5 +1,6 @@
|
|
|
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';
|
|
@@ -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);
|
|
@@ -1626,7 +1636,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1626
1636
|
if (this._descriptor.hasRemoteControl && session.remote_control) {
|
|
1627
1637
|
dot.className = 'jp-AiAssistantsPanel-dot';
|
|
1628
1638
|
statusDotIcon.element({ container: dot });
|
|
1629
|
-
dot.title = 'Remote control session is active';
|
|
1639
|
+
dot.title = this._trans.__('Remote control session is active');
|
|
1630
1640
|
dot.setAttribute('aria-label', dot.title);
|
|
1631
1641
|
return dot;
|
|
1632
1642
|
}
|
|
@@ -1838,6 +1848,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1838
1848
|
var _a;
|
|
1839
1849
|
return ((_a = this._resolvedVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? shieldIcon : undefined;
|
|
1840
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. */
|
|
1841
1857
|
_setupCommands() {
|
|
1842
1858
|
this._commands = new CommandRegistry();
|
|
1843
1859
|
const active = () => this._activeSession;
|
|
@@ -1901,7 +1917,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1901
1917
|
});
|
|
1902
1918
|
}
|
|
1903
1919
|
this._commands.addCommand(this._cmd('open-terminal'), {
|
|
1904
|
-
label: 'Open Terminal',
|
|
1920
|
+
label: this._trans.__('Open Terminal'),
|
|
1905
1921
|
icon: terminalIcon,
|
|
1906
1922
|
execute: () => {
|
|
1907
1923
|
const s = active();
|
|
@@ -1926,7 +1942,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1926
1942
|
}
|
|
1927
1943
|
});
|
|
1928
1944
|
this._commands.addCommand(this._cmd('show-in-filebrowser'), {
|
|
1929
|
-
label: 'Show in File Browser',
|
|
1945
|
+
label: this._trans.__('Show in File Browser'),
|
|
1930
1946
|
icon: folderIcon,
|
|
1931
1947
|
execute: () => {
|
|
1932
1948
|
const s = active();
|
|
@@ -1948,7 +1964,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1948
1964
|
}
|
|
1949
1965
|
});
|
|
1950
1966
|
this._commands.addCommand(this._cmd('copy-path'), {
|
|
1951
|
-
label: 'Copy Path',
|
|
1967
|
+
label: this._trans.__('Copy Path'),
|
|
1952
1968
|
execute: () => {
|
|
1953
1969
|
const s = active();
|
|
1954
1970
|
if (s) {
|
|
@@ -1957,7 +1973,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1957
1973
|
}
|
|
1958
1974
|
});
|
|
1959
1975
|
this._commands.addCommand(this._cmd('copy-session-id'), {
|
|
1960
|
-
label: 'Copy Session ID',
|
|
1976
|
+
label: this._trans.__('Copy Session ID'),
|
|
1961
1977
|
execute: () => {
|
|
1962
1978
|
var _a;
|
|
1963
1979
|
const id = (_a = active()) === null || _a === void 0 ? void 0 : _a.session_id;
|
|
@@ -2089,6 +2105,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2089
2105
|
});
|
|
2090
2106
|
this._buildMenus();
|
|
2091
2107
|
}
|
|
2108
|
+
/* eslint-enable jupyter/command-described-by */
|
|
2092
2109
|
/** How many forced launch variants would actually render right now. Zero
|
|
2093
2110
|
* means every variant builds the launch the plain item already builds, so a
|
|
2094
2111
|
* menu offering both is a menu with one entry. */
|
|
@@ -2108,17 +2125,17 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2108
2125
|
// Lists the project's other conversations. Items are rebuilt on every
|
|
2109
2126
|
// context-menu open from a fresh branches fetch.
|
|
2110
2127
|
this._switchSubmenu = this._menu();
|
|
2111
|
-
this._switchSubmenu.title.label = 'Switch and Manage Sessions';
|
|
2128
|
+
this._switchSubmenu.title.label = this._trans.__('Switch and Manage Sessions');
|
|
2112
2129
|
this._switchSubmenu.title.icon = switchIcon;
|
|
2113
2130
|
// Opens a conversation directly in its own terminal, as opposed to the
|
|
2114
2131
|
// switch submenu which only changes which one the row points at. Several
|
|
2115
2132
|
// conversations can be open at once, independently.
|
|
2116
2133
|
this._openBranchSubmenu = this._menu();
|
|
2117
|
-
this._openBranchSubmenu.title.label = 'Open Branched Conversation';
|
|
2134
|
+
this._openBranchSubmenu.title.label = this._trans.__('Open Branched Conversation');
|
|
2118
2135
|
this._openBranchSubmenu.title.icon = terminalIcon;
|
|
2119
2136
|
// Groups the branch launch modes.
|
|
2120
2137
|
this._branchSessionMenu = this._menu();
|
|
2121
|
-
this._branchSessionMenu.title.label = 'Branch Session';
|
|
2138
|
+
this._branchSessionMenu.title.label = this._trans.__('Branch Session');
|
|
2122
2139
|
this._branchSessionMenu.title.icon = branchIcon;
|
|
2123
2140
|
this._branchSessionMenu.addItem({ command: this._cmd('branch-session') });
|
|
2124
2141
|
for (const mode of this._variantModes) {
|
|
@@ -2335,6 +2352,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2335
2352
|
return;
|
|
2336
2353
|
}
|
|
2337
2354
|
showManageSessionsPopup({
|
|
2355
|
+
trans: this._trans,
|
|
2338
2356
|
branches: this._lastBranches,
|
|
2339
2357
|
current: this._lastBranchesCurrent || session.session_id,
|
|
2340
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
|
@@ -128,10 +128,13 @@ export const filterIcon = new LabIcon({
|
|
|
128
128
|
|
|
129
129
|
// Row status mark - one filled circle, drawn for a conversation that is live
|
|
130
130
|
// or under remote control. An SVG rather than a rounded span so its colour is
|
|
131
|
-
// a CSS `fill` on the circle itself
|
|
132
|
-
//
|
|
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).
|
|
133
136
|
const statusDotSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
|
134
|
-
<circle class="jp-AiAssistantsPanel-dotGlyph" cx="8" cy="8" r="8"
|
|
137
|
+
<circle class="jp-AiAssistantsPanel-dotGlyph" cx="8" cy="8" r="8"/>
|
|
135
138
|
</svg>`;
|
|
136
139
|
|
|
137
140
|
export const statusDotIcon = new LabIcon({
|
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';
|
|
@@ -177,6 +178,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
177
178
|
this._app = options.app;
|
|
178
179
|
this._descriptor = options.descriptor;
|
|
179
180
|
this._hooks = options.hooks ?? {};
|
|
181
|
+
this._trans =
|
|
182
|
+
options.trans ??
|
|
183
|
+
nullTranslator.load('jupyterlab_ai_code_assistants_extension');
|
|
180
184
|
this._serverSettings = options.app.serviceManager.serverSettings;
|
|
181
185
|
this.setRoot(options.rootDir, options.deleteToTrash !== false);
|
|
182
186
|
this._fileBrowser = options.fileBrowser ?? null;
|
|
@@ -190,7 +194,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
190
194
|
colourStore: this._colours,
|
|
191
195
|
terminalTracker: options.terminalTracker ?? null,
|
|
192
196
|
colourfulTabs: options.colourfulTabs ?? null,
|
|
193
|
-
serverSettings: this._serverSettings
|
|
197
|
+
serverSettings: this._serverSettings,
|
|
198
|
+
trans: this._trans
|
|
194
199
|
});
|
|
195
200
|
|
|
196
201
|
this.id = panelWidgetId(this._descriptor.id);
|
|
@@ -364,7 +369,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
364
369
|
|
|
365
370
|
const filterBtn = document.createElement('button');
|
|
366
371
|
filterBtn.className = 'jp-AiAssistantsPanel-iconButton';
|
|
367
|
-
filterBtn.title = 'Filter sessions';
|
|
372
|
+
filterBtn.title = this._trans.__('Filter sessions');
|
|
368
373
|
// The pressed state is drawn with a class, which says nothing to a screen
|
|
369
374
|
// reader; the filter bar starts hidden, so this starts false.
|
|
370
375
|
filterBtn.setAttribute('aria-pressed', 'false');
|
|
@@ -375,7 +380,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
375
380
|
|
|
376
381
|
const refreshBtn = document.createElement('button');
|
|
377
382
|
refreshBtn.className = 'jp-AiAssistantsPanel-iconButton';
|
|
378
|
-
refreshBtn.title = 'Refresh';
|
|
383
|
+
refreshBtn.title = this._trans.__('Refresh');
|
|
379
384
|
refreshIcon.element({ container: refreshBtn });
|
|
380
385
|
refreshBtn.addEventListener('click', () => this.refresh());
|
|
381
386
|
header.appendChild(refreshBtn);
|
|
@@ -395,7 +400,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
395
400
|
search.type = 'search';
|
|
396
401
|
search.className = 'jp-AiAssistantsPanel-search';
|
|
397
402
|
search.placeholder = 'Filter sessions...';
|
|
398
|
-
search.setAttribute('aria-label', 'Filter sessions');
|
|
403
|
+
search.setAttribute('aria-label', this._trans.__('Filter sessions'));
|
|
399
404
|
search.spellcheck = false;
|
|
400
405
|
search.addEventListener('input', () => {
|
|
401
406
|
this._filter = search.value;
|
|
@@ -406,7 +411,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
406
411
|
|
|
407
412
|
const searchClear = document.createElement('button');
|
|
408
413
|
searchClear.className = 'jp-AiAssistantsPanel-searchClear';
|
|
409
|
-
searchClear.title = 'Clear filter';
|
|
414
|
+
searchClear.title = this._trans.__('Clear filter');
|
|
410
415
|
searchClear.hidden = true;
|
|
411
416
|
closeIcon.element({ container: searchClear });
|
|
412
417
|
searchClear.addEventListener('click', () => {
|
|
@@ -697,7 +702,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
697
702
|
`Remove "${name}" from ${this._descriptor.label}? This drops the ` +
|
|
698
703
|
`entire project history and every conversation it holds - ` +
|
|
699
704
|
`${this._disposalVerb}. This cannot be undone.`,
|
|
700
|
-
buttons: [
|
|
705
|
+
buttons: [
|
|
706
|
+
Dialog.cancelButton(),
|
|
707
|
+
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
708
|
+
],
|
|
701
709
|
// Cancel, explicitly. JupyterLab defaults `defaultButton` to the LAST
|
|
702
710
|
// button and focuses it on attach, so omitting this handed the keyboard
|
|
703
711
|
// to Remove - one Enter on a dialog that has just appeared and the
|
|
@@ -737,12 +745,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
737
745
|
const extra = session.extra_sessions;
|
|
738
746
|
const name = this._lookupName(session);
|
|
739
747
|
const confirm = await showDialog({
|
|
740
|
-
title: 'Clean Up Parallel Sessions',
|
|
748
|
+
title: this._trans.__('Clean Up Parallel Sessions'),
|
|
741
749
|
body:
|
|
742
750
|
`Remove ${extra} parallel session${extra === 1 ? '' : 's'} from ` +
|
|
743
751
|
`"${name}"? The current conversation is kept; the rest are ` +
|
|
744
752
|
`${this._disposalVerb}. This cannot be undone.`,
|
|
745
|
-
buttons: [
|
|
753
|
+
buttons: [
|
|
754
|
+
Dialog.cancelButton(),
|
|
755
|
+
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
756
|
+
],
|
|
746
757
|
// Cancel, as above (DEF-51).
|
|
747
758
|
defaultButton: 0
|
|
748
759
|
});
|
|
@@ -768,9 +779,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
768
779
|
body.node.appendChild(bar);
|
|
769
780
|
|
|
770
781
|
const dialog = new Dialog<unknown>({
|
|
771
|
-
title: 'Clean Up Parallel Sessions',
|
|
782
|
+
title: this._trans.__('Clean Up Parallel Sessions'),
|
|
772
783
|
body,
|
|
773
|
-
buttons: [Dialog.okButton({ label: 'Close' })]
|
|
784
|
+
buttons: [Dialog.okButton({ label: this._trans.__('Close') })]
|
|
774
785
|
});
|
|
775
786
|
// Close stays live while the work runs. Hiding it took away the only exit
|
|
776
787
|
// a keyboard user has: the dialog listens for Escape on its own node, it
|
|
@@ -1771,7 +1782,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1771
1782
|
row.classList.add('jp-mod-busy');
|
|
1772
1783
|
const spinner = document.createElement('span');
|
|
1773
1784
|
spinner.className = 'jp-AiAssistantsPanel-spinner';
|
|
1774
|
-
spinner.title = 'Removing...';
|
|
1785
|
+
spinner.title = this._trans.__('Removing...');
|
|
1775
1786
|
row.appendChild(spinner);
|
|
1776
1787
|
} else {
|
|
1777
1788
|
row.appendChild(this._renderIndicator(session));
|
|
@@ -1824,7 +1835,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1824
1835
|
// menu, rather than becoming a second hit target inside the row.
|
|
1825
1836
|
const star = document.createElement('span');
|
|
1826
1837
|
star.className = 'jp-AiAssistantsPanel-favStar';
|
|
1827
|
-
star.setAttribute('aria-label', 'Favorite');
|
|
1838
|
+
star.setAttribute('aria-label', this._trans.__('Favorite'));
|
|
1828
1839
|
star.setAttribute('role', 'img');
|
|
1829
1840
|
starFilledIcon.element({ container: star });
|
|
1830
1841
|
row.appendChild(star);
|
|
@@ -1927,7 +1938,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1927
1938
|
if (this._descriptor.hasRemoteControl && session.remote_control) {
|
|
1928
1939
|
dot.className = 'jp-AiAssistantsPanel-dot';
|
|
1929
1940
|
statusDotIcon.element({ container: dot });
|
|
1930
|
-
dot.title = 'Remote control session is active';
|
|
1941
|
+
dot.title = this._trans.__('Remote control session is active');
|
|
1931
1942
|
dot.setAttribute('aria-label', dot.title);
|
|
1932
1943
|
return dot;
|
|
1933
1944
|
}
|
|
@@ -2169,6 +2180,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2169
2180
|
return this._resolvedVariant()?.unsafe ? shieldIcon : undefined;
|
|
2170
2181
|
}
|
|
2171
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. */
|
|
2172
2189
|
private _setupCommands(): void {
|
|
2173
2190
|
this._commands = new CommandRegistry();
|
|
2174
2191
|
const active = (): ISession | null => this._activeSession;
|
|
@@ -2236,7 +2253,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2236
2253
|
}
|
|
2237
2254
|
|
|
2238
2255
|
this._commands.addCommand(this._cmd('open-terminal'), {
|
|
2239
|
-
label: 'Open Terminal',
|
|
2256
|
+
label: this._trans.__('Open Terminal'),
|
|
2240
2257
|
icon: terminalIcon,
|
|
2241
2258
|
execute: () => {
|
|
2242
2259
|
const s = active();
|
|
@@ -2270,7 +2287,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2270
2287
|
});
|
|
2271
2288
|
|
|
2272
2289
|
this._commands.addCommand(this._cmd('show-in-filebrowser'), {
|
|
2273
|
-
label: 'Show in File Browser',
|
|
2290
|
+
label: this._trans.__('Show in File Browser'),
|
|
2274
2291
|
icon: folderIcon,
|
|
2275
2292
|
execute: () => {
|
|
2276
2293
|
const s = active();
|
|
@@ -2301,7 +2318,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2301
2318
|
});
|
|
2302
2319
|
|
|
2303
2320
|
this._commands.addCommand(this._cmd('copy-path'), {
|
|
2304
|
-
label: 'Copy Path',
|
|
2321
|
+
label: this._trans.__('Copy Path'),
|
|
2305
2322
|
execute: () => {
|
|
2306
2323
|
const s = active();
|
|
2307
2324
|
if (s) {
|
|
@@ -2311,7 +2328,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2311
2328
|
});
|
|
2312
2329
|
|
|
2313
2330
|
this._commands.addCommand(this._cmd('copy-session-id'), {
|
|
2314
|
-
label: 'Copy Session ID',
|
|
2331
|
+
label: this._trans.__('Copy Session ID'),
|
|
2315
2332
|
execute: () => {
|
|
2316
2333
|
const id = active()?.session_id;
|
|
2317
2334
|
if (id) {
|
|
@@ -2451,6 +2468,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2451
2468
|
|
|
2452
2469
|
this._buildMenus();
|
|
2453
2470
|
}
|
|
2471
|
+
/* eslint-enable jupyter/command-described-by */
|
|
2454
2472
|
|
|
2455
2473
|
/** How many forced launch variants would actually render right now. Zero
|
|
2456
2474
|
* means every variant builds the launch the plain item already builds, so a
|
|
@@ -2473,19 +2491,23 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2473
2491
|
// Lists the project's other conversations. Items are rebuilt on every
|
|
2474
2492
|
// context-menu open from a fresh branches fetch.
|
|
2475
2493
|
this._switchSubmenu = this._menu();
|
|
2476
|
-
this._switchSubmenu.title.label =
|
|
2494
|
+
this._switchSubmenu.title.label = this._trans.__(
|
|
2495
|
+
'Switch and Manage Sessions'
|
|
2496
|
+
);
|
|
2477
2497
|
this._switchSubmenu.title.icon = switchIcon;
|
|
2478
2498
|
|
|
2479
2499
|
// Opens a conversation directly in its own terminal, as opposed to the
|
|
2480
2500
|
// switch submenu which only changes which one the row points at. Several
|
|
2481
2501
|
// conversations can be open at once, independently.
|
|
2482
2502
|
this._openBranchSubmenu = this._menu();
|
|
2483
|
-
this._openBranchSubmenu.title.label =
|
|
2503
|
+
this._openBranchSubmenu.title.label = this._trans.__(
|
|
2504
|
+
'Open Branched Conversation'
|
|
2505
|
+
);
|
|
2484
2506
|
this._openBranchSubmenu.title.icon = terminalIcon;
|
|
2485
2507
|
|
|
2486
2508
|
// Groups the branch launch modes.
|
|
2487
2509
|
this._branchSessionMenu = this._menu();
|
|
2488
|
-
this._branchSessionMenu.title.label = 'Branch Session';
|
|
2510
|
+
this._branchSessionMenu.title.label = this._trans.__('Branch Session');
|
|
2489
2511
|
this._branchSessionMenu.title.icon = branchIcon;
|
|
2490
2512
|
this._branchSessionMenu.addItem({ command: this._cmd('branch-session') });
|
|
2491
2513
|
for (const mode of this._variantModes) {
|
|
@@ -2722,6 +2744,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2722
2744
|
return;
|
|
2723
2745
|
}
|
|
2724
2746
|
showManageSessionsPopup({
|
|
2747
|
+
trans: this._trans,
|
|
2725
2748
|
branches: this._lastBranches,
|
|
2726
2749
|
current: this._lastBranchesCurrent || session.session_id,
|
|
2727
2750
|
projectName: this._lookupName(session),
|
|
@@ -2805,6 +2828,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2805
2828
|
|
|
2806
2829
|
private readonly _app: JupyterFrontEnd;
|
|
2807
2830
|
private readonly _descriptor: IProviderDescriptor;
|
|
2831
|
+
private readonly _trans: TranslationBundle;
|
|
2808
2832
|
private readonly _hooks: IProviderHooks;
|
|
2809
2833
|
private readonly _serverSettings: ServerConnection.ISettings;
|
|
2810
2834
|
private _rootDir = '';
|
|
@@ -2869,5 +2893,9 @@ export namespace AssistantSessionsPanel {
|
|
|
2869
2893
|
terminalTracker?: ITerminalTracker | null;
|
|
2870
2894
|
fileBrowser?: IDefaultFileBrowser | null;
|
|
2871
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;
|
|
2872
2900
|
}
|
|
2873
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
|
@@ -366,17 +366,51 @@
|
|
|
366
366
|
height: 8px;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
/* The theme picks the green, through the circle's own fill
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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. */
|
|
376
378
|
.jp-AiAssistantsPanel-dotGlyph {
|
|
377
379
|
fill: var(--jp-success-color1);
|
|
378
380
|
}
|
|
379
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);
|
|
412
|
+
}
|
|
413
|
+
|
|
380
414
|
.jp-AiAssistantsPanel-dotPlaceholder {
|
|
381
415
|
background-color: transparent;
|
|
382
416
|
}
|