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 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 or launching 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
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 and the theme decides it, the way every
109
- // other glyph here is recoloured through its class (DEF-PANE-174).
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" fill="#388e3c"/>
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`,
@@ -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 = (_b = options.fileBrowser) !== null && _b !== void 0 ? _b : null;
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: (_c = options.terminalTracker) !== null && _c !== void 0 ? _c : null,
152
- colourfulTabs: (_d = options.colourfulTabs) !== null && _d !== void 0 ? _d : null,
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: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })],
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: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })],
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),
@@ -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.
@@ -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
  }
@@ -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.7",
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 and the theme decides it, the way every
132
- // other glyph here is recoloured through its class (DEF-PANE-174).
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" fill="#388e3c"/>
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: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })],
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: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })],
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 = 'Switch and Manage Sessions';
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 = 'Open Branched Conversation';
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(sessionId: string): HTMLButtonElement {
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
@@ -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(title: string): Dialog<unknown> {
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(`Opening ${this._descriptor.label}`);
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. This dot is the
370
- ONLY graphic for "a process is running here", so WCAG 1.4.11's 3:1 applies:
371
- color1 is md-green-700 on the light theme and md-green-500 on the dark one,
372
- measuring 4.11:1 on white and 5.79:1 on the dark row. color0 clears the bar
373
- too and was used before, but on the light theme it is md-green-900 - dark
374
- enough at 8px to read as a near-black mark rather than a green light, which
375
- is the whole of what the dot has to say (DEF-PANE-174). */
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
  }