jupyterlab_ai_code_assistants_extension 1.2.7 → 1.2.25

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.
Files changed (44) hide show
  1. package/README.md +10 -7
  2. package/lib/core/icons.js +6 -3
  3. package/lib/core/labels.d.ts +18 -0
  4. package/lib/core/labels.js +68 -0
  5. package/lib/core/panel.d.ts +11 -2
  6. package/lib/core/panel.js +96 -52
  7. package/lib/core/popup.d.ts +8 -2
  8. package/lib/core/popup.js +14 -23
  9. package/lib/core/terminals.d.ts +31 -3
  10. package/lib/core/terminals.js +94 -6
  11. package/lib/core/types.d.ts +16 -4
  12. package/lib/index.js +23 -3
  13. package/lib/providers/claude.js +1 -0
  14. package/lib/providers/codex.js +1 -0
  15. package/lib/providers/deepseek.d.ts +3 -0
  16. package/lib/providers/deepseek.js +63 -0
  17. package/lib/providers/gemini.js +1 -0
  18. package/lib/providers/index.js +8 -1
  19. package/lib/providers/kimi.d.ts +1 -22
  20. package/lib/providers/kimi.js +5 -80
  21. package/package.json +2 -1
  22. package/schema/plugin.json +6 -0
  23. package/src/__tests__/core-neutrality.spec.ts +42 -1
  24. package/src/__tests__/labels.spec.ts +10 -4
  25. package/src/__tests__/launcher.spec.ts +1 -0
  26. package/src/__tests__/panel.spec.ts +172 -0
  27. package/src/__tests__/popup.spec.ts +90 -0
  28. package/src/__tests__/registry.spec.ts +2 -1
  29. package/src/__tests__/tab-colour-capture.spec.ts +1 -0
  30. package/src/__tests__/terminals.spec.ts +266 -0
  31. package/src/core/icons.ts +6 -3
  32. package/src/core/labels.ts +74 -0
  33. package/src/core/panel.ts +115 -46
  34. package/src/core/popup.ts +25 -27
  35. package/src/core/terminals.ts +118 -7
  36. package/src/core/types.ts +17 -4
  37. package/src/index.ts +28 -3
  38. package/src/providers/claude.ts +1 -0
  39. package/src/providers/codex.ts +1 -0
  40. package/src/providers/deepseek.ts +68 -0
  41. package/src/providers/gemini.ts +1 -0
  42. package/src/providers/index.ts +8 -1
  43. package/src/providers/kimi.ts +6 -91
  44. package/style/base.css +52 -8
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  [![Brought To You By KOLOMOLO](https://img.shields.io/badge/Brought%20To%20You%20By-KOLOMOLO-00ffff?style=flat)](https://kolomolo.com)
9
9
  [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-blue?style=flat)](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)
10
10
 
11
- A full launcher and manager for every AI code assistant in JupyterLab - Claude Code, Codex, Kimi and Gemini. Start, resume, fork, switch, and clean up CLI sessions from a side panel per assistant - one click lands you in the right terminal with the assistant already running, no duplicate tabs, no UUID hunting. One install replaces the separate `jupyterlab_claude_code_extension`, `jupyterlab_codex_extension` and `jupyterlab_kimi_code_extension` packages and migrates their settings and favourites automatically.
11
+ A full launcher and manager for every AI code assistant in JupyterLab - Claude Code, Codex, Kimi, Gemini and DeepSeek. Start, resume, fork, switch, and clean up CLI sessions from a side panel per assistant - one click lands you in the right terminal with the assistant already running, no duplicate tabs, no UUID hunting. One install replaces the separate `jupyterlab_claude_code_extension`, `jupyterlab_codex_extension` and `jupyterlab_kimi_code_extension` packages and migrates their settings and favourites automatically.
12
12
 
13
13
  ![Claude Code Sessions panel](.resources/screenshot.png)
14
14
 
@@ -25,17 +25,17 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
25
25
 
26
26
  ## Features
27
27
 
28
- - **One install, every assistant** - Claude Code, Codex, Kimi and Gemini from a single package, each with its own right-side panel wearing its official mark
28
+ - **One install, every assistant** - Claude Code, Codex, Kimi, Gemini and DeepSeek from a single package, each with its own right-side panel wearing its official mark
29
29
  - **Provider registry** - assistant-specific behaviour lives in one module per assistant behind capability flags; no core file names an assistant, and adding one touches no core file
30
30
  - **Joint settings page** - one settings section covering all assistants, with a per-assistant toggle (all on by default); toggling takes effect live, no JupyterLab reload
31
31
  - **Three-section side panel** - Favorites, Recent, and All projects, each scrolling independently
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
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. Exception: a Codex or Kimi conversation started with `+`, and a Codex fork, run without an id until the CLI writes one, so a click on that row meanwhile opens a second terminal
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
- - **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
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, Gemini and DeepSeek) 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
38
- - **Coloured terminal tabs** - each session's colour tints its terminal tab via the companion `jupyterlab_colourful_tab_extension` (installed automatically). Claude's own `/color` supplies its default, Kimi derives a stable colour from the conversation id; Codex and Gemini have no colour of their own until you set one, and a branched session inherits its parent's colour, which it keeps even if the parent's colour later changes or is reset. Tinting needs the companion release that reports the colours you pick and lets another extension own a tab - against an older one this extension tints no tabs at all and says so once, and the companion's own right-click colours keep working as they always did
38
+ - **Coloured terminal tabs** - each session's colour tints its terminal tab via the companion `jupyterlab_colourful_tab_extension` (installed automatically). Claude's own `/color` supplies its default, Kimi derives a stable colour from the conversation id; Codex, Gemini and DeepSeek have no colour of their own until you set one, and a branched session inherits its parent's colour, which it keeps even if the parent's colour later changes or is reset. Tinting needs the companion release that reports the colours you pick and lets another extension own a tab - against an older one this extension tints no tabs at all and says so once, and the companion's own right-click colours keep working as they always did
39
39
  - **Your own tab colour wins** - set a colour on a terminal tab and the extension remembers it for that conversation, overriding whatever the assistant chose. `Reset Tab Colour (n)` in the session's right-click menu hands back every colour you set by hand on that project's conversations, and appears once there is one to hand back. Clearing the colour on the tab itself releases it too, for the conversation that terminal is running; `Reset Tab Colour` is the way back for a conversation with no tab open. A Codex or Kimi conversation that has never been resumed cannot be tracked yet, so a colour set on its tab is not kept
40
40
  - **Favorites** - star projects you keep coming back to via the right-click menu; favourites from the standalone extensions are migrated on first run
41
41
  - **Remove and clean up** - drop a project's history or a project's extra parallel sessions from the right-click menu, confirmation dialog first; removed files honour JupyterLab's "move files to trash" setting
@@ -44,13 +44,14 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
44
44
  - **Search** - fuzzy filter per panel, toggled by the funnel button
45
45
  - **Presentation modes** - label rows by session name or by path relative to the JupyterLab root
46
46
  - **Conflict-safe upgrade** - if a retired standalone extension is still installed, its panel wins and this extension stands down for that assistant instead of showing a duplicate
47
+ - **DeepSeek in the browser** - the DeepSeek Harness has no terminal chat; its panel lists the projects under `~/.dsh/sessions` (or `$DSH_HOME`) and `Open Web UI` starts `dsh --profile web --no-open --port 0` in the project's terminal, which prints the URL to open. The conversation is then chosen in the harness's own browser UI; branching and deleting work from the panel as for every other assistant. The URL is on the machine running JupyterLab, so a remote server needs a port forward to reach it
47
48
  - **Auto-disabled when absent** - an assistant whose CLI is not on `PATH` does not show a panel
48
49
 
49
50
  ## Requirements
50
51
 
51
52
  - JupyterLab >= 4.0.0
52
53
  - Python >= 3.10
53
- - At least one assistant CLI on `PATH`: `claude`, `codex`, `kimi`, or `gemini`
54
+ - At least one assistant CLI on `PATH`: `claude`, `codex`, `kimi`, `gemini`, or `dsh` (the DeepSeek Harness)
54
55
 
55
56
  ## Install
56
57
 
@@ -60,6 +61,8 @@ Developers must install via the project `Makefile` (which orchestrates clean, bu
60
61
  make install
61
62
  ```
62
63
 
64
+ The companion packages are version floors, so `make install` here pulls their published releases and replaces a locally built `jupyterlab_colourful_tab_extension` or `jupyterlab_basic_terminal_extension`. When verifying a change that spans this extension and a companion, install the companion last.
65
+
63
66
  End-users can install the published package from PyPI:
64
67
 
65
68
  ```bash
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`,
@@ -0,0 +1,18 @@
1
+ /** The distinguishing part of a conversation id: its first eight characters
2
+ * past `prefix`, the constant every id of the assistant carries
3
+ * (`IProviderDescriptor.sessionIdPrefix`). Front-sliced when there is no
4
+ * prefix, or when the id does not carry it. */
5
+ export declare function shortSessionId(sessionId: string, prefix?: string): string;
6
+ export declare const MENU_TITLE_COLUMNS = 60;
7
+ /** Trim `text` to `maxColumns` display columns, appending an ellipsis when
8
+ * anything was cut. Iterates by CODE POINT, so an astral character is never
9
+ * split into a lone surrogate (which chromium paints as the replacement
10
+ * glyph). Returns `text` unchanged when it already fits. */
11
+ export declare function truncateToColumns(text: string, maxColumns: number): string;
12
+ /** Menu-item label for one branch: the conversation title trimmed to the
13
+ * column budget, followed by the short session id. Only the TITLE is trimmed -
14
+ * branches of one project share a path, so the id is what tells them apart and
15
+ * it must survive. The bracketed id is dropped when the title already IS the
16
+ * id (the server's last-resort fallback), which can never be truncated since a
17
+ * short id is at most 8 characters. */
18
+ export declare function branchMenuLabel(label: string, shortId: string): string;
@@ -0,0 +1,68 @@
1
+ // Label derivations of the panel: pure string functions, kept free of
2
+ // JupyterLab imports so the unit tests load them without the panel's DOM.
3
+ /** The distinguishing part of a conversation id: its first eight characters
4
+ * past `prefix`, the constant every id of the assistant carries
5
+ * (`IProviderDescriptor.sessionIdPrefix`). Front-sliced when there is no
6
+ * prefix, or when the id does not carry it. */
7
+ export function shortSessionId(sessionId, prefix) {
8
+ const start = prefix && sessionId.startsWith(prefix) ? prefix.length : 0;
9
+ return sessionId.slice(start, start + 8);
10
+ }
11
+ // Lumino sets no `max-width` on `.lm-Menu-itemLabel`, so an uncapped
12
+ // auto-generated conversation title stretches a submenu across the window.
13
+ // Budget is in COLUMNS, not characters. Han, Kana, Hangul and emoji render at
14
+ // roughly one em against Latin's half, so counting characters bounds nothing:
15
+ // 60 Han glyphs measured 851-862px in a real Lumino submenu, at or above the
16
+ // 850px that filed the defect in the first place. An assistant whose titles
17
+ // are model-generated makes a wide-script title the expected case.
18
+ export const MENU_TITLE_COLUMNS = 60;
19
+ // East Asian Wide and Fullwidth ranges, plus emoji. Deliberately coarse: this
20
+ // bounds a menu width, so a handful of mis-classified glyphs cost pixels, not
21
+ // correctness. Anything outside these ranges counts as one column.
22
+ const WIDE_RANGES = [
23
+ [0x1100, 0x115f], // Hangul Jamo
24
+ [0x2e80, 0x303e], // CJK radicals, Kangxi, CJK symbols and punctuation
25
+ [0x3041, 0x33ff], // Hiragana, Katakana, Bopomofo, Hangul Compat, CJK Compat
26
+ [0x3400, 0x4dbf], // CJK Unified Extension A
27
+ [0x4e00, 0x9fff], // CJK Unified Ideographs
28
+ [0xa000, 0xa4cf], // Yi
29
+ [0xac00, 0xd7a3], // Hangul syllables
30
+ [0xf900, 0xfaff], // CJK Compatibility Ideographs
31
+ [0xfe30, 0xfe6f], // CJK Compatibility Forms
32
+ [0xff00, 0xff60], // Fullwidth forms
33
+ [0xffe0, 0xffe6], // Fullwidth signs
34
+ [0x1f300, 0x1faff], // Emoji and pictographs
35
+ [0x20000, 0x3fffd] // CJK Unified Extensions B and beyond
36
+ ];
37
+ function isWide(codePoint) {
38
+ return WIDE_RANGES.some(([lo, hi]) => codePoint >= lo && codePoint <= hi);
39
+ }
40
+ /** Trim `text` to `maxColumns` display columns, appending an ellipsis when
41
+ * anything was cut. Iterates by CODE POINT, so an astral character is never
42
+ * split into a lone surrogate (which chromium paints as the replacement
43
+ * glyph). Returns `text` unchanged when it already fits. */
44
+ export function truncateToColumns(text, maxColumns) {
45
+ let columns = 0;
46
+ let kept = '';
47
+ for (const ch of text) {
48
+ const width = isWide(ch.codePointAt(0)) ? 2 : 1;
49
+ if (columns + width > maxColumns) {
50
+ // The cut can land just after a space; drop it so the ellipsis hugs the
51
+ // last word. `trimEnd` is ES2019 and this project targets ES2018.
52
+ return `${kept.replace(/\s+$/, '')}…`;
53
+ }
54
+ columns += width;
55
+ kept += ch;
56
+ }
57
+ return text;
58
+ }
59
+ /** Menu-item label for one branch: the conversation title trimmed to the
60
+ * column budget, followed by the short session id. Only the TITLE is trimmed -
61
+ * branches of one project share a path, so the id is what tells them apart and
62
+ * it must survive. The bracketed id is dropped when the title already IS the
63
+ * id (the server's last-resort fallback), which can never be truncated since a
64
+ * short id is at most 8 characters. */
65
+ export function branchMenuLabel(label, shortId) {
66
+ const title = truncateToColumns(label, MENU_TITLE_COLUMNS);
67
+ return label === shortId ? title : `${title} (${shortId})`;
68
+ }
@@ -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';
@@ -78,6 +79,10 @@ export declare class AssistantSessionsPanel extends Widget {
78
79
  private _logError;
79
80
  private _fetch;
80
81
  private _toggleFavourite;
82
+ /** The warning a permanent deletion earns and a trashed one does not - a
83
+ * trashed conversation is recoverable, and the popup one click away says
84
+ * so; the same sentence on both would be false half the time. */
85
+ private get _irreversibleNote();
81
86
  private get _disposalVerb();
82
87
  private _removeProject;
83
88
  private _cleanupParallel;
@@ -211,8 +216,7 @@ export declare class AssistantSessionsPanel extends Widget {
211
216
  * cannot give. */
212
217
  private _formatAbsoluteTime;
213
218
  /** Branch entry display: conversation name plus short session id. Branches
214
- * share a project path, so the name and id are all that tell them apart. The
215
- * suffix is dropped when the label already IS the short id. */
219
+ * share a project path, so the name and id are all that tell them apart. */
216
220
  private _branchDisplayName;
217
221
  /** Menu-item label for a branch: display name, relative time, and a trailing
218
222
  * marker when a live worker owns the conversation. The marker lives in the
@@ -303,6 +307,7 @@ export declare class AssistantSessionsPanel extends Widget {
303
307
  private _stopPolling;
304
308
  private readonly _app;
305
309
  private readonly _descriptor;
310
+ private readonly _trans;
306
311
  private readonly _hooks;
307
312
  private readonly _serverSettings;
308
313
  private _rootDir;
@@ -366,5 +371,9 @@ export declare namespace AssistantSessionsPanel {
366
371
  terminalTracker?: ITerminalTracker | null;
367
372
  fileBrowser?: IDefaultFileBrowser | null;
368
373
  colourfulTabs?: IColourfulTabs | null;
374
+ /** Bundle every user-visible string passes through. Optional so a
375
+ * caller that does not translate keeps working - the null bundle
376
+ * returns each string unchanged. */
377
+ trans?: TranslationBundle;
369
378
  }
370
379
  }
package/lib/core/panel.js CHANGED
@@ -1,9 +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';
8
+ import { branchMenuLabel, shortSessionId } from './labels';
7
9
  import { addIcon, branchIcon, cleanupIcon, filterIcon, providerIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon, statusDotIcon, switchIcon } from './icons';
8
10
  import { DEFAULT_RECENT_LIMIT, MAX_RECENT_LIMIT, MIN_RECENT_LIMIT } from './limits';
9
11
  import { resolveLaunchMode, resolvedLaunchModeEntry } from './modes';
@@ -97,7 +99,7 @@ function saveExpanded(storageKey, state) {
97
99
  */
98
100
  export class AssistantSessionsPanel extends Widget {
99
101
  constructor(options) {
100
- var _a, _b, _c, _d;
102
+ var _a, _b, _c, _d, _e;
101
103
  super();
102
104
  this._rootDir = '';
103
105
  this._deleteToTrash = true;
@@ -138,9 +140,11 @@ export class AssistantSessionsPanel extends Widget {
138
140
  this._app = options.app;
139
141
  this._descriptor = options.descriptor;
140
142
  this._hooks = (_a = options.hooks) !== null && _a !== void 0 ? _a : {};
143
+ this._trans =
144
+ (_b = options.trans) !== null && _b !== void 0 ? _b : nullTranslator.load('jupyterlab_ai_code_assistants_extension');
141
145
  this._serverSettings = options.app.serviceManager.serverSettings;
142
146
  this.setRoot(options.rootDir, options.deleteToTrash !== false);
143
- this._fileBrowser = (_b = options.fileBrowser) !== null && _b !== void 0 ? _b : null;
147
+ this._fileBrowser = (_c = options.fileBrowser) !== null && _c !== void 0 ? _c : null;
144
148
  this._expandedKey = `jupyterlab_ai_code_assistants_extension:${this._descriptor.id}:expanded`;
145
149
  this._expanded = loadExpanded(this._expandedKey);
146
150
  this._colours = new ColourStore(this._descriptor.id, this._serverSettings);
@@ -148,9 +152,10 @@ export class AssistantSessionsPanel extends Widget {
148
152
  app: options.app,
149
153
  descriptor: this._descriptor,
150
154
  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
155
+ terminalTracker: (_d = options.terminalTracker) !== null && _d !== void 0 ? _d : null,
156
+ colourfulTabs: (_e = options.colourfulTabs) !== null && _e !== void 0 ? _e : null,
157
+ serverSettings: this._serverSettings,
158
+ trans: this._trans
154
159
  });
155
160
  this.id = panelWidgetId(this._descriptor.id);
156
161
  this.title.icon = providerIcon(this._descriptor.iconName, this._descriptor.iconSvg);
@@ -299,7 +304,7 @@ export class AssistantSessionsPanel extends Widget {
299
304
  header.appendChild(newBtn);
300
305
  const filterBtn = document.createElement('button');
301
306
  filterBtn.className = 'jp-AiAssistantsPanel-iconButton';
302
- filterBtn.title = 'Filter sessions';
307
+ filterBtn.title = this._trans.__('Filter sessions');
303
308
  // The pressed state is drawn with a class, which says nothing to a screen
304
309
  // reader; the filter bar starts hidden, so this starts false.
305
310
  filterBtn.setAttribute('aria-pressed', 'false');
@@ -309,7 +314,7 @@ export class AssistantSessionsPanel extends Widget {
309
314
  this._filterBtn = filterBtn;
310
315
  const refreshBtn = document.createElement('button');
311
316
  refreshBtn.className = 'jp-AiAssistantsPanel-iconButton';
312
- refreshBtn.title = 'Refresh';
317
+ refreshBtn.title = this._trans.__('Refresh');
313
318
  refreshIcon.element({ container: refreshBtn });
314
319
  refreshBtn.addEventListener('click', () => this.refresh());
315
320
  header.appendChild(refreshBtn);
@@ -327,7 +332,7 @@ export class AssistantSessionsPanel extends Widget {
327
332
  search.type = 'search';
328
333
  search.className = 'jp-AiAssistantsPanel-search';
329
334
  search.placeholder = 'Filter sessions...';
330
- search.setAttribute('aria-label', 'Filter sessions');
335
+ search.setAttribute('aria-label', this._trans.__('Filter sessions'));
331
336
  search.spellcheck = false;
332
337
  search.addEventListener('input', () => {
333
338
  this._filter = search.value;
@@ -337,7 +342,7 @@ export class AssistantSessionsPanel extends Widget {
337
342
  this._searchEl = search;
338
343
  const searchClear = document.createElement('button');
339
344
  searchClear.className = 'jp-AiAssistantsPanel-searchClear';
340
- searchClear.title = 'Clear filter';
345
+ searchClear.title = this._trans.__('Clear filter');
341
346
  searchClear.hidden = true;
342
347
  closeIcon.element({ container: searchClear });
343
348
  searchClear.addEventListener('click', () => {
@@ -582,6 +587,12 @@ export class AssistantSessionsPanel extends Widget {
582
587
  this._showActionError('Could not toggle favorite - try again.', err);
583
588
  }
584
589
  }
590
+ /** The warning a permanent deletion earns and a trashed one does not - a
591
+ * trashed conversation is recoverable, and the popup one click away says
592
+ * so; the same sentence on both would be false half the time. */
593
+ get _irreversibleNote() {
594
+ return this._deleteToTrash ? '' : ' This cannot be undone.';
595
+ }
585
596
  get _disposalVerb() {
586
597
  return this._deleteToTrash ? 'moved to trash' : 'deleted permanently';
587
598
  }
@@ -592,8 +603,11 @@ export class AssistantSessionsPanel extends Widget {
592
603
  title: `Remove from ${this._descriptor.label}`,
593
604
  body: `Remove "${name}" from ${this._descriptor.label}? This drops the ` +
594
605
  `entire project history and every conversation it holds - ` +
595
- `${this._disposalVerb}. This cannot be undone.`,
596
- buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })],
606
+ `${this._disposalVerb}.${this._irreversibleNote}`,
607
+ buttons: [
608
+ Dialog.cancelButton(),
609
+ Dialog.warnButton({ label: this._trans.__('Remove') })
610
+ ],
597
611
  // Cancel, explicitly. JupyterLab defaults `defaultButton` to the LAST
598
612
  // button and focuses it on attach, so omitting this handed the keyboard
599
613
  // to Remove - one Enter on a dialog that has just appeared and the
@@ -627,11 +641,14 @@ export class AssistantSessionsPanel extends Widget {
627
641
  const extra = session.extra_sessions;
628
642
  const name = this._lookupName(session);
629
643
  const confirm = await showDialog({
630
- title: 'Clean Up Parallel Sessions',
644
+ title: this._trans.__('Clean Up Parallel Sessions'),
631
645
  body: `Remove ${extra} parallel session${extra === 1 ? '' : 's'} from ` +
632
646
  `"${name}"? The current conversation is kept; the rest are ` +
633
- `${this._disposalVerb}. This cannot be undone.`,
634
- buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })],
647
+ `${this._disposalVerb}.${this._irreversibleNote}`,
648
+ buttons: [
649
+ Dialog.cancelButton(),
650
+ Dialog.warnButton({ label: this._trans.__('Remove') })
651
+ ],
635
652
  // Cancel, as above (DEF-51).
636
653
  defaultButton: 0
637
654
  });
@@ -651,9 +668,9 @@ export class AssistantSessionsPanel extends Widget {
651
668
  bar.max = 1;
652
669
  body.node.appendChild(bar);
653
670
  const dialog = new Dialog({
654
- title: 'Clean Up Parallel Sessions',
671
+ title: this._trans.__('Clean Up Parallel Sessions'),
655
672
  body,
656
- buttons: [Dialog.okButton({ label: 'Close' })]
673
+ buttons: [Dialog.okButton({ label: this._trans.__('Close') })]
657
674
  });
658
675
  // Close stays live while the work runs. Hiding it took away the only exit
659
676
  // a keyboard user has: the dialog listens for Escape on its own node, it
@@ -829,7 +846,7 @@ export class AssistantSessionsPanel extends Widget {
829
846
  if (row) {
830
847
  // A terminal already holding that conversation IS the answer: a second
831
848
  // process on one history is never what the click meant (ACC-LNCH-152).
832
- const found = await this._terminals.findForSession(row.session_id);
849
+ const found = await this._terminals.findForSession(row.session_id, folder);
833
850
  if (found) {
834
851
  this._terminals.focus(found.widget);
835
852
  return found.widget;
@@ -1184,6 +1201,7 @@ export class AssistantSessionsPanel extends Widget {
1184
1201
  }
1185
1202
  /** Make another conversation the row's current one. */
1186
1203
  async _switchBranch(session, sessionId) {
1204
+ var _a;
1187
1205
  try {
1188
1206
  const result = await requestProvider(this._descriptor.id, 'switch', this._serverSettings, {
1189
1207
  method: 'POST',
@@ -1195,6 +1213,15 @@ export class AssistantSessionsPanel extends Widget {
1195
1213
  if (result.current !== result.requested) {
1196
1214
  Notification.warning('That conversation could not be made the current one.', { autoClose: 4000 });
1197
1215
  }
1216
+ else {
1217
+ // The re-render is the only other signal, and it can read as the
1218
+ // opposite of success: the row now reports the conversation's true
1219
+ // last activity, so switching to an old one dims the row and drops
1220
+ // it out of Recent (DEF-PANE-181).
1221
+ const target = this._lastBranches.find(b => b.session_id === sessionId);
1222
+ const label = (_a = target === null || target === void 0 ? void 0 : target.label) !== null && _a !== void 0 ? _a : shortSessionId(sessionId, this._descriptor.sessionIdPrefix);
1223
+ Notification.success(`${this._lookupName(session)}: switched to ${label}`, { autoClose: 3000 });
1224
+ }
1198
1225
  }
1199
1226
  catch (err) {
1200
1227
  Notification.error(isResponseStatus(err, 404)
@@ -1476,17 +1503,6 @@ export class AssistantSessionsPanel extends Widget {
1476
1503
  row.classList.add('jp-mod-stale');
1477
1504
  }
1478
1505
  }
1479
- const removing = this._removingPaths.has(session.encoded_path);
1480
- if (removing) {
1481
- row.classList.add('jp-mod-busy');
1482
- const spinner = document.createElement('span');
1483
- spinner.className = 'jp-AiAssistantsPanel-spinner';
1484
- spinner.title = 'Removing...';
1485
- row.appendChild(spinner);
1486
- }
1487
- else {
1488
- row.appendChild(this._renderIndicator(session));
1489
- }
1490
1506
  const name = document.createElement('span');
1491
1507
  name.className = 'jp-AiAssistantsPanel-name';
1492
1508
  // The text ellipsises in a span of its own so the badges after it survive
@@ -1521,6 +1537,21 @@ export class AssistantSessionsPanel extends Widget {
1521
1537
  name.appendChild(bg);
1522
1538
  }
1523
1539
  row.appendChild(name);
1540
+ // The status dot (or the removal spinner in its place) leads the row
1541
+ // visually - CSS `order` puts it first - but follows the name in the DOM,
1542
+ // so a screen reader announces the project before its status sentence
1543
+ // rather than the same preamble ahead of every active row (DEF-PANE-183).
1544
+ const removing = this._removingPaths.has(session.encoded_path);
1545
+ if (removing) {
1546
+ row.classList.add('jp-mod-busy');
1547
+ const spinner = document.createElement('span');
1548
+ spinner.className = 'jp-AiAssistantsPanel-spinner';
1549
+ spinner.title = this._trans.__('Removing...');
1550
+ row.appendChild(spinner);
1551
+ }
1552
+ else {
1553
+ row.appendChild(this._renderIndicator(session));
1554
+ }
1524
1555
  // No star in the Favorites section - every row there is one by definition.
1525
1556
  // The star sits before the time so the fixed-width time column stays the
1526
1557
  // rightmost alignment anchor across all rows.
@@ -1531,7 +1562,7 @@ export class AssistantSessionsPanel extends Widget {
1531
1562
  // menu, rather than becoming a second hit target inside the row.
1532
1563
  const star = document.createElement('span');
1533
1564
  star.className = 'jp-AiAssistantsPanel-favStar';
1534
- star.setAttribute('aria-label', 'Favorite');
1565
+ star.setAttribute('aria-label', this._trans.__('Favorite'));
1535
1566
  star.setAttribute('role', 'img');
1536
1567
  starFilledIcon.element({ container: star });
1537
1568
  row.appendChild(star);
@@ -1626,7 +1657,7 @@ export class AssistantSessionsPanel extends Widget {
1626
1657
  if (this._descriptor.hasRemoteControl && session.remote_control) {
1627
1658
  dot.className = 'jp-AiAssistantsPanel-dot';
1628
1659
  statusDotIcon.element({ container: dot });
1629
- dot.title = 'Remote control session is active';
1660
+ dot.title = this._trans.__('Remote control session is active');
1630
1661
  dot.setAttribute('aria-label', dot.title);
1631
1662
  return dot;
1632
1663
  }
@@ -1749,14 +1780,9 @@ export class AssistantSessionsPanel extends Widget {
1749
1780
  `${pad(d.getHours())}:${pad(d.getMinutes())}`);
1750
1781
  }
1751
1782
  /** Branch entry display: conversation name plus short session id. Branches
1752
- * share a project path, so the name and id are all that tell them apart. The
1753
- * suffix is dropped when the label already IS the short id. */
1783
+ * share a project path, so the name and id are all that tell them apart. */
1754
1784
  _branchDisplayName(b) {
1755
- const shortId = b.session_id.slice(0, 8);
1756
- if (this._hooks.branchLabel) {
1757
- return this._hooks.branchLabel(b, shortId);
1758
- }
1759
- return b.label === shortId ? b.label : `${b.label} (${shortId})`;
1785
+ return branchMenuLabel(b.label, shortSessionId(b.session_id, this._descriptor.sessionIdPrefix));
1760
1786
  }
1761
1787
  /** Menu-item label for a branch: display name, relative time, and a trailing
1762
1788
  * marker when a live worker owns the conversation. The marker lives in the
@@ -1838,6 +1864,12 @@ export class AssistantSessionsPanel extends Widget {
1838
1864
  var _a;
1839
1865
  return ((_a = this._resolvedVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? shieldIcon : undefined;
1840
1866
  }
1867
+ /* eslint-disable jupyter/command-described-by --
1868
+ These commands go on the private CommandRegistry built below, which only
1869
+ this panel's own row context menu reads. `describedBy` is metadata for
1870
+ the application registry's consumers - the palette, settings, other
1871
+ extensions - and none of them can see or invoke a command registered
1872
+ here. */
1841
1873
  _setupCommands() {
1842
1874
  this._commands = new CommandRegistry();
1843
1875
  const active = () => this._activeSession;
@@ -1901,7 +1933,7 @@ export class AssistantSessionsPanel extends Widget {
1901
1933
  });
1902
1934
  }
1903
1935
  this._commands.addCommand(this._cmd('open-terminal'), {
1904
- label: 'Open Terminal',
1936
+ label: this._trans.__('Open Terminal'),
1905
1937
  icon: terminalIcon,
1906
1938
  execute: () => {
1907
1939
  const s = active();
@@ -1926,7 +1958,7 @@ export class AssistantSessionsPanel extends Widget {
1926
1958
  }
1927
1959
  });
1928
1960
  this._commands.addCommand(this._cmd('show-in-filebrowser'), {
1929
- label: 'Show in File Browser',
1961
+ label: this._trans.__('Show in File Browser'),
1930
1962
  icon: folderIcon,
1931
1963
  execute: () => {
1932
1964
  const s = active();
@@ -1948,7 +1980,7 @@ export class AssistantSessionsPanel extends Widget {
1948
1980
  }
1949
1981
  });
1950
1982
  this._commands.addCommand(this._cmd('copy-path'), {
1951
- label: 'Copy Path',
1983
+ label: this._trans.__('Copy Path'),
1952
1984
  execute: () => {
1953
1985
  const s = active();
1954
1986
  if (s) {
@@ -1957,7 +1989,7 @@ export class AssistantSessionsPanel extends Widget {
1957
1989
  }
1958
1990
  });
1959
1991
  this._commands.addCommand(this._cmd('copy-session-id'), {
1960
- label: 'Copy Session ID',
1992
+ label: this._trans.__('Copy Session ID'),
1961
1993
  execute: () => {
1962
1994
  var _a;
1963
1995
  const id = (_a = active()) === null || _a === void 0 ? void 0 : _a.session_id;
@@ -2089,6 +2121,7 @@ export class AssistantSessionsPanel extends Widget {
2089
2121
  });
2090
2122
  this._buildMenus();
2091
2123
  }
2124
+ /* eslint-enable jupyter/command-described-by */
2092
2125
  /** How many forced launch variants would actually render right now. Zero
2093
2126
  * means every variant builds the launch the plain item already builds, so a
2094
2127
  * menu offering both is a menu with one entry. */
@@ -2108,17 +2141,17 @@ export class AssistantSessionsPanel extends Widget {
2108
2141
  // Lists the project's other conversations. Items are rebuilt on every
2109
2142
  // context-menu open from a fresh branches fetch.
2110
2143
  this._switchSubmenu = this._menu();
2111
- this._switchSubmenu.title.label = 'Switch and Manage Sessions';
2144
+ this._switchSubmenu.title.label = this._trans.__('Switch and Manage Sessions');
2112
2145
  this._switchSubmenu.title.icon = switchIcon;
2113
2146
  // Opens a conversation directly in its own terminal, as opposed to the
2114
2147
  // switch submenu which only changes which one the row points at. Several
2115
2148
  // conversations can be open at once, independently.
2116
2149
  this._openBranchSubmenu = this._menu();
2117
- this._openBranchSubmenu.title.label = 'Open Branched Conversation';
2150
+ this._openBranchSubmenu.title.label = this._trans.__('Open Branched Conversation');
2118
2151
  this._openBranchSubmenu.title.icon = terminalIcon;
2119
2152
  // Groups the branch launch modes.
2120
2153
  this._branchSessionMenu = this._menu();
2121
- this._branchSessionMenu.title.label = 'Branch Session';
2154
+ this._branchSessionMenu.title.label = this._trans.__('Branch Session');
2122
2155
  this._branchSessionMenu.title.icon = branchIcon;
2123
2156
  this._branchSessionMenu.addItem({ command: this._cmd('branch-session') });
2124
2157
  for (const mode of this._variantModes) {
@@ -2169,10 +2202,14 @@ export class AssistantSessionsPanel extends Widget {
2169
2202
  add('reset-colour');
2170
2203
  this._contextMenu.addItem({ type: 'separator' });
2171
2204
  if (withBranches) {
2172
- this._contextMenu.addItem({
2173
- type: 'submenu',
2174
- submenu: this._openBranchSubmenu
2175
- });
2205
+ // A project-scoped assistant has one terminal for every conversation,
2206
+ // so opening a branch in its own terminal is not an action it has.
2207
+ if (this._descriptor.terminalScope === 'conversation') {
2208
+ this._contextMenu.addItem({
2209
+ type: 'submenu',
2210
+ submenu: this._openBranchSubmenu
2211
+ });
2212
+ }
2176
2213
  this._contextMenu.addItem({
2177
2214
  type: 'submenu',
2178
2215
  submenu: this._switchSubmenu
@@ -2334,10 +2371,12 @@ export class AssistantSessionsPanel extends Widget {
2334
2371
  if (!session) {
2335
2372
  return;
2336
2373
  }
2374
+ const current = this._lastBranchesCurrent || session.session_id;
2337
2375
  showManageSessionsPopup({
2376
+ trans: this._trans,
2338
2377
  branches: this._lastBranches,
2339
- current: this._lastBranchesCurrent || session.session_id,
2340
- projectName: this._lookupName(session),
2378
+ current,
2379
+ currentName: `${this._lookupName(session)} (${shortSessionId(current, this._descriptor.sessionIdPrefix)})`,
2341
2380
  deleteToTrash: this._deleteToTrash,
2342
2381
  branchName: b => this._branchDisplayName(b),
2343
2382
  formatTime: ms => this._formatRelativeTime(ms),
@@ -2358,11 +2397,16 @@ export class AssistantSessionsPanel extends Widget {
2358
2397
  * is attached to and carries none. The row's own conversation is not in
2359
2398
  * `_lastBranches`, so it answers from the session. */
2360
2399
  _openBranchTitle(session, sessionId) {
2361
- var _a;
2400
+ var _a, _b, _c, _d;
2401
+ // One terminal serves a project-scoped assistant, so the button focuses
2402
+ // or starts it, under the provider's own verb for that.
2403
+ if (this._descriptor.terminalScope === 'project') {
2404
+ return ((_c = (_b = (_a = this._hooks).resumeLabel) === null || _b === void 0 ? void 0 : _b.call(_a, session)) !== null && _c !== void 0 ? _c : "Open the project's terminal");
2405
+ }
2362
2406
  const base = 'Open this conversation in its own terminal';
2363
2407
  const target = sessionId === session.session_id
2364
2408
  ? session
2365
- : ((_a = this._lastBranches.find(b => b.session_id === sessionId)) !== null && _a !== void 0 ? _a : null);
2409
+ : ((_d = this._lastBranches.find(b => b.session_id === sessionId)) !== null && _d !== void 0 ? _d : null);
2366
2410
  const variant = this._resumeVariant(target);
2367
2411
  return variant ? `${base} (${variant.label})` : base;
2368
2412
  }
@@ -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,12 +10,17 @@ 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. */
15
20
  current: string;
16
- /** Display name of the row this popup was opened from. */
17
- projectName: string;
21
+ /** Display name of the current row: the project's name and the short id
22
+ * of `current`, derived by the panel with the same rule as `branchName`. */
23
+ currentName: string;
18
24
  /** Whether deletions move to trash - the popup says which it means. */
19
25
  deleteToTrash: boolean;
20
26
  /** Display name for one branch, `<label> (<short id>)` by default. */