jupyterlab_ai_code_assistants_extension 1.2.5 → 1.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
@@ -9,6 +9,7 @@ export declare const addIcon: LabIcon;
9
9
  export declare const branchIcon: LabIcon;
10
10
  export declare const switchIcon: LabIcon;
11
11
  export declare const filterIcon: LabIcon;
12
+ export declare const statusDotIcon: LabIcon;
12
13
  /** Build (or reuse) the LabIcon for a provider's own glyph. */
13
14
  export declare function providerIcon(name: string, svgstr: string): LabIcon;
14
15
  /**
package/lib/core/icons.js CHANGED
@@ -103,6 +103,20 @@ export const filterIcon = new LabIcon({
103
103
  name: `${ICON_PREFIX}:filter`,
104
104
  svgstr: filterSvgStr
105
105
  });
106
+ // Row status mark - one filled circle, drawn for a conversation that is live
107
+ // or under remote control. An SVG rather than a rounded span so its colour is
108
+ // a CSS `fill` on the circle itself, the way every other glyph here is
109
+ // recoloured through its class (DEF-PANE-174). The circle carries no `fill`
110
+ // attribute: the stylesheet is the single source of that colour, and a
111
+ // presentation attribute here would be a second one to keep in step
112
+ // (DEF-PANE-179).
113
+ const statusDotSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
114
+ <circle class="jp-AiAssistantsPanel-dotGlyph" cx="8" cy="8" r="8"/>
115
+ </svg>`;
116
+ export const statusDotIcon = new LabIcon({
117
+ name: `${ICON_PREFIX}:status-dot`,
118
+ svgstr: statusDotSvgStr
119
+ });
106
120
  // Provider glyphs already built, by icon name. LabIcon warns and returns the
107
121
  // first registration for a duplicate name, so a provider re-registered by a
108
122
  // live settings toggle must resolve to the icon it built the first time
@@ -2,6 +2,7 @@ import { JupyterFrontEnd } from '@jupyterlab/application';
2
2
  import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
3
3
  import { ITerminalTracker } from '@jupyterlab/terminal';
4
4
  import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
5
+ import { TranslationBundle } from '@jupyterlab/translation';
5
6
  import { Message } from '@lumino/messaging';
6
7
  import { Widget } from '@lumino/widgets';
7
8
  import { IProviderDescriptor, IProviderHooks } from './types';
@@ -303,6 +304,7 @@ export declare class AssistantSessionsPanel extends Widget {
303
304
  private _stopPolling;
304
305
  private readonly _app;
305
306
  private readonly _descriptor;
307
+ private readonly _trans;
306
308
  private readonly _hooks;
307
309
  private readonly _serverSettings;
308
310
  private _rootDir;
@@ -366,5 +368,9 @@ export declare namespace AssistantSessionsPanel {
366
368
  terminalTracker?: ITerminalTracker | null;
367
369
  fileBrowser?: IDefaultFileBrowser | null;
368
370
  colourfulTabs?: IColourfulTabs | null;
371
+ /** Bundle every user-visible string passes through. Optional so a
372
+ * caller that does not translate keeps working - the null bundle
373
+ * returns each string unchanged. */
374
+ trans?: TranslationBundle;
369
375
  }
370
376
  }
package/lib/core/panel.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { Clipboard, Dialog, InputDialog, Notification, showDialog } from '@jupyterlab/apputils';
2
2
  import { MenuSvg, closeIcon, folderIcon, terminalIcon } from '@jupyterlab/ui-components';
3
+ import { nullTranslator } from '@jupyterlab/translation';
3
4
  import { CommandRegistry } from '@lumino/commands';
4
5
  import { UUID } from '@lumino/coreutils';
5
6
  import { Widget } from '@lumino/widgets';
6
7
  import { ColourStore } from './colour';
7
- import { addIcon, branchIcon, cleanupIcon, filterIcon, providerIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon, switchIcon } from './icons';
8
+ import { addIcon, branchIcon, cleanupIcon, filterIcon, providerIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon, statusDotIcon, switchIcon } from './icons';
8
9
  import { DEFAULT_RECENT_LIMIT, MAX_RECENT_LIMIT, MIN_RECENT_LIMIT } from './limits';
9
10
  import { resolveLaunchMode, resolvedLaunchModeEntry } from './modes';
10
11
  import { showManageSessionsPopup } from './popup';
@@ -97,7 +98,7 @@ function saveExpanded(storageKey, state) {
97
98
  */
98
99
  export class AssistantSessionsPanel extends Widget {
99
100
  constructor(options) {
100
- var _a, _b, _c, _d;
101
+ var _a, _b, _c, _d, _e;
101
102
  super();
102
103
  this._rootDir = '';
103
104
  this._deleteToTrash = true;
@@ -138,9 +139,11 @@ export class AssistantSessionsPanel extends Widget {
138
139
  this._app = options.app;
139
140
  this._descriptor = options.descriptor;
140
141
  this._hooks = (_a = options.hooks) !== null && _a !== void 0 ? _a : {};
142
+ this._trans =
143
+ (_b = options.trans) !== null && _b !== void 0 ? _b : nullTranslator.load('jupyterlab_ai_code_assistants_extension');
141
144
  this._serverSettings = options.app.serviceManager.serverSettings;
142
145
  this.setRoot(options.rootDir, options.deleteToTrash !== false);
143
- this._fileBrowser = (_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);
@@ -1625,12 +1635,14 @@ export class AssistantSessionsPanel extends Widget {
1625
1635
  // words go on an aria-label.
1626
1636
  if (this._descriptor.hasRemoteControl && session.remote_control) {
1627
1637
  dot.className = 'jp-AiAssistantsPanel-dot';
1628
- dot.title = 'Remote control session is active';
1638
+ statusDotIcon.element({ container: dot });
1639
+ dot.title = this._trans.__('Remote control session is active');
1629
1640
  dot.setAttribute('aria-label', dot.title);
1630
1641
  return dot;
1631
1642
  }
1632
1643
  if (this._descriptor.hasLiveProcess && session.live) {
1633
1644
  dot.className = 'jp-AiAssistantsPanel-dot';
1645
+ statusDotIcon.element({ container: dot });
1634
1646
  dot.title = `A ${this._descriptor.label} session is active in this project`;
1635
1647
  dot.setAttribute('aria-label', dot.title);
1636
1648
  return dot;
@@ -1836,6 +1848,12 @@ export class AssistantSessionsPanel extends Widget {
1836
1848
  var _a;
1837
1849
  return ((_a = this._resolvedVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? shieldIcon : undefined;
1838
1850
  }
1851
+ /* eslint-disable jupyter/command-described-by --
1852
+ These commands go on the private CommandRegistry built below, which only
1853
+ this panel's own row context menu reads. `describedBy` is metadata for
1854
+ the application registry's consumers - the palette, settings, other
1855
+ extensions - and none of them can see or invoke a command registered
1856
+ here. */
1839
1857
  _setupCommands() {
1840
1858
  this._commands = new CommandRegistry();
1841
1859
  const active = () => this._activeSession;
@@ -1899,7 +1917,7 @@ export class AssistantSessionsPanel extends Widget {
1899
1917
  });
1900
1918
  }
1901
1919
  this._commands.addCommand(this._cmd('open-terminal'), {
1902
- label: 'Open Terminal',
1920
+ label: this._trans.__('Open Terminal'),
1903
1921
  icon: terminalIcon,
1904
1922
  execute: () => {
1905
1923
  const s = active();
@@ -1924,7 +1942,7 @@ export class AssistantSessionsPanel extends Widget {
1924
1942
  }
1925
1943
  });
1926
1944
  this._commands.addCommand(this._cmd('show-in-filebrowser'), {
1927
- label: 'Show in File Browser',
1945
+ label: this._trans.__('Show in File Browser'),
1928
1946
  icon: folderIcon,
1929
1947
  execute: () => {
1930
1948
  const s = active();
@@ -1946,7 +1964,7 @@ export class AssistantSessionsPanel extends Widget {
1946
1964
  }
1947
1965
  });
1948
1966
  this._commands.addCommand(this._cmd('copy-path'), {
1949
- label: 'Copy Path',
1967
+ label: this._trans.__('Copy Path'),
1950
1968
  execute: () => {
1951
1969
  const s = active();
1952
1970
  if (s) {
@@ -1955,7 +1973,7 @@ export class AssistantSessionsPanel extends Widget {
1955
1973
  }
1956
1974
  });
1957
1975
  this._commands.addCommand(this._cmd('copy-session-id'), {
1958
- label: 'Copy Session ID',
1976
+ label: this._trans.__('Copy Session ID'),
1959
1977
  execute: () => {
1960
1978
  var _a;
1961
1979
  const id = (_a = active()) === null || _a === void 0 ? void 0 : _a.session_id;
@@ -2087,6 +2105,7 @@ export class AssistantSessionsPanel extends Widget {
2087
2105
  });
2088
2106
  this._buildMenus();
2089
2107
  }
2108
+ /* eslint-enable jupyter/command-described-by */
2090
2109
  /** How many forced launch variants would actually render right now. Zero
2091
2110
  * means every variant builds the launch the plain item already builds, so a
2092
2111
  * menu offering both is a menu with one entry. */
@@ -2106,17 +2125,17 @@ export class AssistantSessionsPanel extends Widget {
2106
2125
  // Lists the project's other conversations. Items are rebuilt on every
2107
2126
  // context-menu open from a fresh branches fetch.
2108
2127
  this._switchSubmenu = this._menu();
2109
- this._switchSubmenu.title.label = 'Switch and Manage Sessions';
2128
+ this._switchSubmenu.title.label = this._trans.__('Switch and Manage Sessions');
2110
2129
  this._switchSubmenu.title.icon = switchIcon;
2111
2130
  // Opens a conversation directly in its own terminal, as opposed to the
2112
2131
  // switch submenu which only changes which one the row points at. Several
2113
2132
  // conversations can be open at once, independently.
2114
2133
  this._openBranchSubmenu = this._menu();
2115
- this._openBranchSubmenu.title.label = 'Open Branched Conversation';
2134
+ this._openBranchSubmenu.title.label = this._trans.__('Open Branched Conversation');
2116
2135
  this._openBranchSubmenu.title.icon = terminalIcon;
2117
2136
  // Groups the branch launch modes.
2118
2137
  this._branchSessionMenu = this._menu();
2119
- this._branchSessionMenu.title.label = 'Branch Session';
2138
+ this._branchSessionMenu.title.label = this._trans.__('Branch Session');
2120
2139
  this._branchSessionMenu.title.icon = branchIcon;
2121
2140
  this._branchSessionMenu.addItem({ command: this._cmd('branch-session') });
2122
2141
  for (const mode of this._variantModes) {
@@ -2333,6 +2352,7 @@ export class AssistantSessionsPanel extends Widget {
2333
2352
  return;
2334
2353
  }
2335
2354
  showManageSessionsPopup({
2355
+ trans: this._trans,
2336
2356
  branches: this._lastBranches,
2337
2357
  current: this._lastBranchesCurrent || session.session_id,
2338
2358
  projectName: this._lookupName(session),
@@ -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.5",
3
+ "version": "1.2.18",
4
4
  "description": "Jupyterlab extension to replace individual jupyterlab_*_code_extension and integrate into one, that offers joint capability, joint settings page and individual right toolbar panels per code assistant, with the ability in settings to decide which code assistant support is on (default - all are enabled)",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -66,6 +66,7 @@
66
66
  "@jupyterlab/services": "^7.0.0",
67
67
  "@jupyterlab/settingregistry": "^4.0.0",
68
68
  "@jupyterlab/terminal": "^4.0.0",
69
+ "@jupyterlab/translation": "^4.0.0",
69
70
  "@jupyterlab/ui-components": "^4.0.0",
70
71
  "@lumino/commands": "^2.0.0",
71
72
  "@lumino/coreutils": "^2.0.0",
package/src/core/icons.ts CHANGED
@@ -126,6 +126,22 @@ export const filterIcon = new LabIcon({
126
126
  svgstr: filterSvgStr
127
127
  });
128
128
 
129
+ // Row status mark - one filled circle, drawn for a conversation that is live
130
+ // or under remote control. An SVG rather than a rounded span so its colour is
131
+ // a CSS `fill` on the circle itself, the way every other glyph here is
132
+ // recoloured through its class (DEF-PANE-174). The circle carries no `fill`
133
+ // attribute: the stylesheet is the single source of that colour, and a
134
+ // presentation attribute here would be a second one to keep in step
135
+ // (DEF-PANE-179).
136
+ const statusDotSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
137
+ <circle class="jp-AiAssistantsPanel-dotGlyph" cx="8" cy="8" r="8"/>
138
+ </svg>`;
139
+
140
+ export const statusDotIcon = new LabIcon({
141
+ name: `${ICON_PREFIX}:status-dot`,
142
+ svgstr: statusDotSvgStr
143
+ });
144
+
129
145
  // Provider glyphs already built, by icon name. LabIcon warns and returns the
130
146
  // first registration for a duplicate name, so a provider re-registered by a
131
147
  // live settings toggle must resolve to the icon it built the first time
package/src/core/panel.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  folderIcon,
18
18
  terminalIcon
19
19
  } from '@jupyterlab/ui-components';
20
+ import { TranslationBundle, nullTranslator } from '@jupyterlab/translation';
20
21
  import { CommandRegistry } from '@lumino/commands';
21
22
  import { UUID } from '@lumino/coreutils';
22
23
  import { Message } from '@lumino/messaging';
@@ -33,6 +34,7 @@ import {
33
34
  removeIcon,
34
35
  shieldIcon,
35
36
  starFilledIcon,
37
+ statusDotIcon,
36
38
  switchIcon
37
39
  } from './icons';
38
40
  import {
@@ -176,6 +178,9 @@ export class AssistantSessionsPanel extends Widget {
176
178
  this._app = options.app;
177
179
  this._descriptor = options.descriptor;
178
180
  this._hooks = options.hooks ?? {};
181
+ this._trans =
182
+ options.trans ??
183
+ nullTranslator.load('jupyterlab_ai_code_assistants_extension');
179
184
  this._serverSettings = options.app.serviceManager.serverSettings;
180
185
  this.setRoot(options.rootDir, options.deleteToTrash !== false);
181
186
  this._fileBrowser = options.fileBrowser ?? null;
@@ -189,7 +194,8 @@ export class AssistantSessionsPanel extends Widget {
189
194
  colourStore: this._colours,
190
195
  terminalTracker: options.terminalTracker ?? null,
191
196
  colourfulTabs: options.colourfulTabs ?? null,
192
- serverSettings: this._serverSettings
197
+ serverSettings: this._serverSettings,
198
+ trans: this._trans
193
199
  });
194
200
 
195
201
  this.id = panelWidgetId(this._descriptor.id);
@@ -363,7 +369,7 @@ export class AssistantSessionsPanel extends Widget {
363
369
 
364
370
  const filterBtn = document.createElement('button');
365
371
  filterBtn.className = 'jp-AiAssistantsPanel-iconButton';
366
- filterBtn.title = 'Filter sessions';
372
+ filterBtn.title = this._trans.__('Filter sessions');
367
373
  // The pressed state is drawn with a class, which says nothing to a screen
368
374
  // reader; the filter bar starts hidden, so this starts false.
369
375
  filterBtn.setAttribute('aria-pressed', 'false');
@@ -374,7 +380,7 @@ export class AssistantSessionsPanel extends Widget {
374
380
 
375
381
  const refreshBtn = document.createElement('button');
376
382
  refreshBtn.className = 'jp-AiAssistantsPanel-iconButton';
377
- refreshBtn.title = 'Refresh';
383
+ refreshBtn.title = this._trans.__('Refresh');
378
384
  refreshIcon.element({ container: refreshBtn });
379
385
  refreshBtn.addEventListener('click', () => this.refresh());
380
386
  header.appendChild(refreshBtn);
@@ -394,7 +400,7 @@ export class AssistantSessionsPanel extends Widget {
394
400
  search.type = 'search';
395
401
  search.className = 'jp-AiAssistantsPanel-search';
396
402
  search.placeholder = 'Filter sessions...';
397
- search.setAttribute('aria-label', 'Filter sessions');
403
+ search.setAttribute('aria-label', this._trans.__('Filter sessions'));
398
404
  search.spellcheck = false;
399
405
  search.addEventListener('input', () => {
400
406
  this._filter = search.value;
@@ -405,7 +411,7 @@ export class AssistantSessionsPanel extends Widget {
405
411
 
406
412
  const searchClear = document.createElement('button');
407
413
  searchClear.className = 'jp-AiAssistantsPanel-searchClear';
408
- searchClear.title = 'Clear filter';
414
+ searchClear.title = this._trans.__('Clear filter');
409
415
  searchClear.hidden = true;
410
416
  closeIcon.element({ container: searchClear });
411
417
  searchClear.addEventListener('click', () => {
@@ -696,7 +702,10 @@ export class AssistantSessionsPanel extends Widget {
696
702
  `Remove "${name}" from ${this._descriptor.label}? This drops the ` +
697
703
  `entire project history and every conversation it holds - ` +
698
704
  `${this._disposalVerb}. This cannot be undone.`,
699
- buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })],
705
+ buttons: [
706
+ Dialog.cancelButton(),
707
+ Dialog.warnButton({ label: this._trans.__('Remove') })
708
+ ],
700
709
  // Cancel, explicitly. JupyterLab defaults `defaultButton` to the LAST
701
710
  // button and focuses it on attach, so omitting this handed the keyboard
702
711
  // to Remove - one Enter on a dialog that has just appeared and the
@@ -736,12 +745,15 @@ export class AssistantSessionsPanel extends Widget {
736
745
  const extra = session.extra_sessions;
737
746
  const name = this._lookupName(session);
738
747
  const confirm = await showDialog({
739
- title: 'Clean Up Parallel Sessions',
748
+ title: this._trans.__('Clean Up Parallel Sessions'),
740
749
  body:
741
750
  `Remove ${extra} parallel session${extra === 1 ? '' : 's'} from ` +
742
751
  `"${name}"? The current conversation is kept; the rest are ` +
743
752
  `${this._disposalVerb}. This cannot be undone.`,
744
- buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })],
753
+ buttons: [
754
+ Dialog.cancelButton(),
755
+ Dialog.warnButton({ label: this._trans.__('Remove') })
756
+ ],
745
757
  // Cancel, as above (DEF-51).
746
758
  defaultButton: 0
747
759
  });
@@ -767,9 +779,9 @@ export class AssistantSessionsPanel extends Widget {
767
779
  body.node.appendChild(bar);
768
780
 
769
781
  const dialog = new Dialog<unknown>({
770
- title: 'Clean Up Parallel Sessions',
782
+ title: this._trans.__('Clean Up Parallel Sessions'),
771
783
  body,
772
- buttons: [Dialog.okButton({ label: 'Close' })]
784
+ buttons: [Dialog.okButton({ label: this._trans.__('Close') })]
773
785
  });
774
786
  // Close stays live while the work runs. Hiding it took away the only exit
775
787
  // a keyboard user has: the dialog listens for Escape on its own node, it
@@ -1770,7 +1782,7 @@ export class AssistantSessionsPanel extends Widget {
1770
1782
  row.classList.add('jp-mod-busy');
1771
1783
  const spinner = document.createElement('span');
1772
1784
  spinner.className = 'jp-AiAssistantsPanel-spinner';
1773
- spinner.title = 'Removing...';
1785
+ spinner.title = this._trans.__('Removing...');
1774
1786
  row.appendChild(spinner);
1775
1787
  } else {
1776
1788
  row.appendChild(this._renderIndicator(session));
@@ -1823,7 +1835,7 @@ export class AssistantSessionsPanel extends Widget {
1823
1835
  // menu, rather than becoming a second hit target inside the row.
1824
1836
  const star = document.createElement('span');
1825
1837
  star.className = 'jp-AiAssistantsPanel-favStar';
1826
- star.setAttribute('aria-label', 'Favorite');
1838
+ star.setAttribute('aria-label', this._trans.__('Favorite'));
1827
1839
  star.setAttribute('role', 'img');
1828
1840
  starFilledIcon.element({ container: star });
1829
1841
  row.appendChild(star);
@@ -1925,12 +1937,14 @@ export class AssistantSessionsPanel extends Widget {
1925
1937
  // words go on an aria-label.
1926
1938
  if (this._descriptor.hasRemoteControl && session.remote_control) {
1927
1939
  dot.className = 'jp-AiAssistantsPanel-dot';
1928
- dot.title = 'Remote control session is active';
1940
+ statusDotIcon.element({ container: dot });
1941
+ dot.title = this._trans.__('Remote control session is active');
1929
1942
  dot.setAttribute('aria-label', dot.title);
1930
1943
  return dot;
1931
1944
  }
1932
1945
  if (this._descriptor.hasLiveProcess && session.live) {
1933
1946
  dot.className = 'jp-AiAssistantsPanel-dot';
1947
+ statusDotIcon.element({ container: dot });
1934
1948
  dot.title = `A ${this._descriptor.label} session is active in this project`;
1935
1949
  dot.setAttribute('aria-label', dot.title);
1936
1950
  return dot;
@@ -2166,6 +2180,12 @@ export class AssistantSessionsPanel extends Widget {
2166
2180
  return this._resolvedVariant()?.unsafe ? shieldIcon : undefined;
2167
2181
  }
2168
2182
 
2183
+ /* eslint-disable jupyter/command-described-by --
2184
+ These commands go on the private CommandRegistry built below, which only
2185
+ this panel's own row context menu reads. `describedBy` is metadata for
2186
+ the application registry's consumers - the palette, settings, other
2187
+ extensions - and none of them can see or invoke a command registered
2188
+ here. */
2169
2189
  private _setupCommands(): void {
2170
2190
  this._commands = new CommandRegistry();
2171
2191
  const active = (): ISession | null => this._activeSession;
@@ -2233,7 +2253,7 @@ export class AssistantSessionsPanel extends Widget {
2233
2253
  }
2234
2254
 
2235
2255
  this._commands.addCommand(this._cmd('open-terminal'), {
2236
- label: 'Open Terminal',
2256
+ label: this._trans.__('Open Terminal'),
2237
2257
  icon: terminalIcon,
2238
2258
  execute: () => {
2239
2259
  const s = active();
@@ -2267,7 +2287,7 @@ export class AssistantSessionsPanel extends Widget {
2267
2287
  });
2268
2288
 
2269
2289
  this._commands.addCommand(this._cmd('show-in-filebrowser'), {
2270
- label: 'Show in File Browser',
2290
+ label: this._trans.__('Show in File Browser'),
2271
2291
  icon: folderIcon,
2272
2292
  execute: () => {
2273
2293
  const s = active();
@@ -2298,7 +2318,7 @@ export class AssistantSessionsPanel extends Widget {
2298
2318
  });
2299
2319
 
2300
2320
  this._commands.addCommand(this._cmd('copy-path'), {
2301
- label: 'Copy Path',
2321
+ label: this._trans.__('Copy Path'),
2302
2322
  execute: () => {
2303
2323
  const s = active();
2304
2324
  if (s) {
@@ -2308,7 +2328,7 @@ export class AssistantSessionsPanel extends Widget {
2308
2328
  });
2309
2329
 
2310
2330
  this._commands.addCommand(this._cmd('copy-session-id'), {
2311
- label: 'Copy Session ID',
2331
+ label: this._trans.__('Copy Session ID'),
2312
2332
  execute: () => {
2313
2333
  const id = active()?.session_id;
2314
2334
  if (id) {
@@ -2448,6 +2468,7 @@ export class AssistantSessionsPanel extends Widget {
2448
2468
 
2449
2469
  this._buildMenus();
2450
2470
  }
2471
+ /* eslint-enable jupyter/command-described-by */
2451
2472
 
2452
2473
  /** How many forced launch variants would actually render right now. Zero
2453
2474
  * means every variant builds the launch the plain item already builds, so a
@@ -2470,19 +2491,23 @@ export class AssistantSessionsPanel extends Widget {
2470
2491
  // Lists the project's other conversations. Items are rebuilt on every
2471
2492
  // context-menu open from a fresh branches fetch.
2472
2493
  this._switchSubmenu = this._menu();
2473
- this._switchSubmenu.title.label = 'Switch and Manage Sessions';
2494
+ this._switchSubmenu.title.label = this._trans.__(
2495
+ 'Switch and Manage Sessions'
2496
+ );
2474
2497
  this._switchSubmenu.title.icon = switchIcon;
2475
2498
 
2476
2499
  // Opens a conversation directly in its own terminal, as opposed to the
2477
2500
  // switch submenu which only changes which one the row points at. Several
2478
2501
  // conversations can be open at once, independently.
2479
2502
  this._openBranchSubmenu = this._menu();
2480
- this._openBranchSubmenu.title.label = 'Open Branched Conversation';
2503
+ this._openBranchSubmenu.title.label = this._trans.__(
2504
+ 'Open Branched Conversation'
2505
+ );
2481
2506
  this._openBranchSubmenu.title.icon = terminalIcon;
2482
2507
 
2483
2508
  // Groups the branch launch modes.
2484
2509
  this._branchSessionMenu = this._menu();
2485
- this._branchSessionMenu.title.label = 'Branch Session';
2510
+ this._branchSessionMenu.title.label = this._trans.__('Branch Session');
2486
2511
  this._branchSessionMenu.title.icon = branchIcon;
2487
2512
  this._branchSessionMenu.addItem({ command: this._cmd('branch-session') });
2488
2513
  for (const mode of this._variantModes) {
@@ -2719,6 +2744,7 @@ export class AssistantSessionsPanel extends Widget {
2719
2744
  return;
2720
2745
  }
2721
2746
  showManageSessionsPopup({
2747
+ trans: this._trans,
2722
2748
  branches: this._lastBranches,
2723
2749
  current: this._lastBranchesCurrent || session.session_id,
2724
2750
  projectName: this._lookupName(session),
@@ -2802,6 +2828,7 @@ export class AssistantSessionsPanel extends Widget {
2802
2828
 
2803
2829
  private readonly _app: JupyterFrontEnd;
2804
2830
  private readonly _descriptor: IProviderDescriptor;
2831
+ private readonly _trans: TranslationBundle;
2805
2832
  private readonly _hooks: IProviderHooks;
2806
2833
  private readonly _serverSettings: ServerConnection.ISettings;
2807
2834
  private _rootDir = '';
@@ -2866,5 +2893,9 @@ export namespace AssistantSessionsPanel {
2866
2893
  terminalTracker?: ITerminalTracker | null;
2867
2894
  fileBrowser?: IDefaultFileBrowser | null;
2868
2895
  colourfulTabs?: IColourfulTabs | null;
2896
+ /** Bundle every user-visible string passes through. Optional so a
2897
+ * caller that does not translate keeps working - the null bundle
2898
+ * returns each string unchanged. */
2899
+ trans?: TranslationBundle;
2869
2900
  }
2870
2901
  }
package/src/core/popup.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Clipboard, Dialog } from '@jupyterlab/apputils';
2
+ import { TranslationBundle, nullTranslator } from '@jupyterlab/translation';
2
3
  import { copyIcon } from '@jupyterlab/ui-components';
3
4
  import { Widget } from '@lumino/widgets';
4
5
 
@@ -15,6 +16,10 @@ import { IBranch } from './types';
15
16
  */
16
17
  export namespace ManageSessionsPopup {
17
18
  export interface IOptions {
19
+ /** Bundle every user-visible string passes through. Optional so a
20
+ * caller that does not translate keeps working - the null bundle
21
+ * returns each string unchanged. */
22
+ trans?: TranslationBundle;
18
23
  /** Conversations other than the current one. */
19
24
  branches: IBranch[];
20
25
  /** The project's current conversation id, pinned at the top. */
@@ -50,11 +55,14 @@ export namespace ManageSessionsPopup {
50
55
 
51
56
  /** Compact copy button for a popup row. `stopPropagation` keeps the click from
52
57
  * switching or selecting the row it sits in. */
53
- function copyButton(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
@@ -349,17 +349,66 @@
349
349
  flex: 0 0 auto;
350
350
  width: 8px;
351
351
  height: 8px;
352
- border-radius: 50%;
353
352
  display: inline-block;
354
353
  }
355
354
 
356
355
  .jp-AiAssistantsPanel-dot {
357
- /* color0, at full opacity. This dot is the ONLY graphic for "a process is
358
- running here", so WCAG 1.4.11's 3:1 applies to it; softened over color1 it
359
- measured 2.12:1 on the stock light theme. color0 is the darker token, so
360
- it reads quieter than a saturated color1 while clearing the bar. */
361
- background-color: var(--jp-success-color0);
362
- box-shadow: 0 0 4px var(--jp-success-color0);
356
+ display: inline-flex;
357
+ align-items: center;
358
+
359
+ /* drop-shadow, not box-shadow: the glyph is an svg now, and box-shadow would
360
+ halo its square element box rather than the circle inside it. */
361
+ filter: drop-shadow(0 0 2px var(--jp-success-color1));
362
+ }
363
+
364
+ .jp-AiAssistantsPanel-dot svg {
365
+ width: 8px;
366
+ height: 8px;
367
+ }
368
+
369
+ /* The theme picks the green, through the circle's own fill rather than a
370
+ presentation attribute on the svg, the way every other glyph here is
371
+ recoloured (DEF-PANE-174). This dot is the ONLY graphic for "a process is
372
+ running here", so WCAG 1.4.11's 3:1 applies: color1 is md-green-500 on every
373
+ dark theme installed, 5.79:1 against the stock dark row. color0 clears the
374
+ bar too and was used before, but it is a step darker again and at 8px reads
375
+ as a near-black mark rather than a green light, which is the whole of what
376
+ the dot has to say. The light themes need a different answer entirely - see
377
+ the rule below. */
378
+ .jp-AiAssistantsPanel-dotGlyph {
379
+ fill: var(--jp-success-color1);
380
+ }
381
+
382
+ /* Light themes take a different dot, because both halves of the one above stop
383
+ working there. JupyterLab stamps the theme's own `isLight` onto the body, so
384
+ the switch is CSS and no TypeScript sees it.
385
+
386
+ Fill - `--jp-success-color1` is a dark green on every light theme installed
387
+ (md-green-700 on JupyterLab Light, #27632a on the others), which is the
388
+ near-black mark the rule above rejects (DEF-PANE-175). md-green-500 is the
389
+ green the dark themes already carry, defined on `:root` by JupyterLab's own
390
+ materialcolors.css, so no fallback copy belongs here.
391
+
392
+ Glow - a mint ring, md-green-200, three passes at 2px. The dot's own green
393
+ would separate nothing from the dot, and white was tried and rejected on
394
+ sight (DEF-PANE-201): on the mid-grey rows the installed light themes draw
395
+ (#bec3ca, #c0c0c0) a white ring reads as a bright blob, not a light. A pale
396
+ green three steps lighter than the fill reads as the fill's own glow instead.
397
+ Three passes because drop-shadow spreads its alpha over the blur, so one
398
+ pass stays faint; 2px because at 1px the ring is barely wider than the
399
+ circle's anti-aliasing and at 3px stacked deeper it washes over the first
400
+ letter of the name. Size and shade chosen from rendered candidates on the
401
+ live row (2026-09-07). Stated plainly: no pale halo reaches WCAG 1.4.11's
402
+ 3:1 on those rows - pure white against #bec3ca is 1.77:1, the ceiling - and
403
+ the dark contour that would was rejected on sight (DEF-PANE-185). */
404
+ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dot {
405
+ filter: drop-shadow(0 0 2px var(--md-green-200))
406
+ drop-shadow(0 0 2px var(--md-green-200))
407
+ drop-shadow(0 0 2px var(--md-green-200));
408
+ }
409
+
410
+ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotGlyph {
411
+ fill: var(--md-green-500);
363
412
  }
364
413
 
365
414
  .jp-AiAssistantsPanel-dotPlaceholder {