jupyterlab_ai_code_assistants_extension 1.2.123 → 1.2.199

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
@@ -78,7 +78,7 @@ pip install jupyterlab_ai_code_assistants_extension
78
78
 
79
79
  ## Agent skill
80
80
 
81
- The wheel ships a session-operations skill for coding agents at `share/jupyter/agents/skills/jupyterlab-ai-code-assistants-extension/SKILL.md`. It covers finding, moving, splitting, renaming and promoting sessions, copying a session into another project under a fresh id, the task-list environment variables, and running Claude Code against Kimi K3 or DeepSeek.
81
+ The wheel ships a session-operations skill for coding agents at `share/jupyter/agents/skills/jupyterlab-ai-code-assistants-extension/SKILL.md`. It covers finding, moving, splitting, renaming and promoting sessions, copying a session into another project under a fresh id, the task-list environment variables, and running Claude Code against Kimi K3 or DeepSeek, with two references for the rest - `claude-operations.md` for how `claude -c` and `--resume` choose a session and how to repair, promote and pin one, and `cross-assistant-sessions.md` for reading, inventorying, taking over and resuming sessions of Claude Code, Codex, Kimi, Gemini and DeepSeek.
82
82
 
83
83
  After `pip install`, with the Python that runs the lab:
84
84
 
@@ -1,5 +1,6 @@
1
1
  import { Dialog, Notification } from '@jupyterlab/apputils';
2
2
  import { Widget } from '@lumino/widgets';
3
+ import { dialogRenderer } from './dialogs';
3
4
  import { requestAPI } from './request';
4
5
  /** The command that delivers a pasted login callback link. */
5
6
  export const CALLBACK_COMMAND = 'open:callback';
@@ -71,7 +72,8 @@ class CallbackDialog extends Dialog {
71
72
  Dialog.okButton({ label: _trans.__('OK') })
72
73
  ],
73
74
  defaultButton: 1,
74
- focusNodeSelector: 'input'
75
+ focusNodeSelector: 'input',
76
+ renderer: dialogRenderer
75
77
  });
76
78
  this._form = _form;
77
79
  this._send = _send;
@@ -0,0 +1,18 @@
1
+ import { Dialog } from '@jupyterlab/apputils';
2
+ import { Widget } from '@lumino/widgets';
3
+ /**
4
+ * The renderer every dialog of this extension is built with. It marks the
5
+ * footer, and `style/base.css` scopes its dialog rules - no focus rim on a
6
+ * button focused by the mouse or by a dialog opened with the mouse (keyboard
7
+ * focus keeps JupyterLab's rim), the reject buttons (Cancel, and Close in the
8
+ * Open Callback popup) as
9
+ * secondary surface buttons so the fill alone marks the primary - to a dialog
10
+ * that holds the mark, so JupyterLab's own
11
+ * dialogs keep their look. A renderer is the one hook that also reaches the
12
+ * dialogs JupyterLab builds on our behalf: `InputDialog.getText` passes its
13
+ * options through, and takes no class.
14
+ */
15
+ export declare class DialogRenderer extends Dialog.Renderer {
16
+ createFooter(buttons: ReadonlyArray<HTMLElement>, checkbox: HTMLElement | null): Widget;
17
+ }
18
+ export declare const dialogRenderer: DialogRenderer;
@@ -0,0 +1,21 @@
1
+ import { Dialog } from '@jupyterlab/apputils';
2
+ /**
3
+ * The renderer every dialog of this extension is built with. It marks the
4
+ * footer, and `style/base.css` scopes its dialog rules - no focus rim on a
5
+ * button focused by the mouse or by a dialog opened with the mouse (keyboard
6
+ * focus keeps JupyterLab's rim), the reject buttons (Cancel, and Close in the
7
+ * Open Callback popup) as
8
+ * secondary surface buttons so the fill alone marks the primary - to a dialog
9
+ * that holds the mark, so JupyterLab's own
10
+ * dialogs keep their look. A renderer is the one hook that also reaches the
11
+ * dialogs JupyterLab builds on our behalf: `InputDialog.getText` passes its
12
+ * options through, and takes no class.
13
+ */
14
+ export class DialogRenderer extends Dialog.Renderer {
15
+ createFooter(buttons, checkbox) {
16
+ const footer = super.createFooter(buttons, checkbox);
17
+ footer.addClass('jp-AiAssistantsPanel-dialogFooter');
18
+ return footer;
19
+ }
20
+ }
21
+ export const dialogRenderer = new DialogRenderer();
@@ -10,7 +10,15 @@ export declare const branchIcon: LabIcon;
10
10
  export declare const switchIcon: LabIcon;
11
11
  export declare const filterIcon: LabIcon;
12
12
  export declare const statusDotIcon: LabIcon;
13
- /** Build (or reuse) the LabIcon for a provider's own glyph. */
13
+ /**
14
+ * Build (or reuse) the LabIcon for a provider's own glyph.
15
+ *
16
+ * Every provider SVG carries a square viewBox centred on its art, with the
17
+ * art's larger side at 20/24 of the box - the inset of JupyterLab's own icons
18
+ * (the terminal icon draws 2..22 in a 24 box). The Launcher card, the sidebar
19
+ * tab and the menus each draw every icon in one box size, so the same inset
20
+ * gives every assistant the same visual size there (DEF-PANE-332).
21
+ */
14
22
  export declare function providerIcon(name: string, svgstr: string): LabIcon;
15
23
  /**
16
24
  * A provider's tile icon as the Launcher should draw it.
@@ -20,7 +28,11 @@ export declare function providerIcon(name: string, svgstr: string): LabIcon;
20
28
  * preset, and each tile under `launcherCard`. This view of `tile` renders the
21
29
  * extension's joint icon under the section preset and `tile` everywhere else,
22
30
  * so the header carries no vendor's mark while every tile keeps its own
23
- * (ACC-LNCH-163).
31
+ * (ACC-LNCH-163). Under `launcherCard` it also adds the class
32
+ * `jp-AiAssistantsLauncher-tileGlyph`, which `style/base.css` scales to 75%,
33
+ * so the tile glyph is drawn smaller than JupyterLab's own tile glyphs while
34
+ * the sidebar tab, panel header and menus keep the standard size
35
+ * (ACC-LNCH-215).
24
36
  *
25
37
  * Built the way `LabIcon.bindprops` builds its views - a prototype child of
26
38
  * the tile with its own `react` - so `LabIcon.resolve` still recognises it.
package/lib/core/icons.js CHANGED
@@ -1,4 +1,4 @@
1
- import { LabIcon } from '@jupyterlab/ui-components';
1
+ import { classes, LabIcon } from '@jupyterlab/ui-components';
2
2
  import * as React from 'react';
3
3
  // Shared panel chrome icons. Provider icons are NOT here - each provider
4
4
  // carries its own SVG on its descriptor and the registry builds the LabIcon
@@ -123,7 +123,15 @@ export const statusDotIcon = new LabIcon({
123
123
  // live settings toggle must resolve to the icon it built the first time
124
124
  // rather than construct a second one on every toggle.
125
125
  const _providerIcons = new Map();
126
- /** Build (or reuse) the LabIcon for a provider's own glyph. */
126
+ /**
127
+ * Build (or reuse) the LabIcon for a provider's own glyph.
128
+ *
129
+ * Every provider SVG carries a square viewBox centred on its art, with the
130
+ * art's larger side at 20/24 of the box - the inset of JupyterLab's own icons
131
+ * (the terminal icon draws 2..22 in a 24 box). The Launcher card, the sidebar
132
+ * tab and the menus each draw every icon in one box size, so the same inset
133
+ * gives every assistant the same visual size there (DEF-PANE-332).
134
+ */
127
135
  export function providerIcon(name, svgstr) {
128
136
  let icon = _providerIcons.get(name);
129
137
  if (!icon) {
@@ -140,7 +148,11 @@ export function providerIcon(name, svgstr) {
140
148
  * preset, and each tile under `launcherCard`. This view of `tile` renders the
141
149
  * extension's joint icon under the section preset and `tile` everywhere else,
142
150
  * so the header carries no vendor's mark while every tile keeps its own
143
- * (ACC-LNCH-163).
151
+ * (ACC-LNCH-163). Under `launcherCard` it also adds the class
152
+ * `jp-AiAssistantsLauncher-tileGlyph`, which `style/base.css` scales to 75%,
153
+ * so the tile glyph is drawn smaller than JupyterLab's own tile glyphs while
154
+ * the sidebar tab, panel header and menus keep the standard size
155
+ * (ACC-LNCH-215).
144
156
  *
145
157
  * Built the way `LabIcon.bindprops` builds its views - a prototype child of
146
158
  * the tile with its own `react` - so `LabIcon.resolve` still recognises it.
@@ -149,7 +161,13 @@ export function launcherTileIcon(tile) {
149
161
  const view = Object.create(tile);
150
162
  const react = React.forwardRef((props, ref) => React.createElement(props.stylesheet === 'launcherSection'
151
163
  ? assistantsIcon.react
152
- : tile.react, { ...props, ref }));
164
+ : tile.react, {
165
+ ...props,
166
+ ...(props.stylesheet === 'launcherCard' && {
167
+ className: classes(props.className, 'jp-AiAssistantsLauncher-tileGlyph')
168
+ }),
169
+ ref
170
+ }));
153
171
  // `react` is readonly on the type; the view shadows the tile's own.
154
172
  Object.defineProperty(view, 'react', { value: react });
155
173
  return view;
@@ -45,14 +45,6 @@ export declare class AssistantSessionsPanel extends Widget {
45
45
  protected onBeforeHide(_msg: Message): void;
46
46
  protected onCloseRequest(msg: Message): void;
47
47
  private _buildShell;
48
- /** Raise the click-time busy overlay. Each action gets a token, because an
49
- * open and a branch can overlap; the overlay clears when the last one ends,
50
- * and the label names the newest action still running. */
51
- private _beginBusy;
52
- private _endBusy;
53
- /** Name the newest still-running action in the visible label and the live
54
- * region, so neither keeps naming an action that ended. */
55
- private _paintBusyMessage;
56
48
  /** Show / hide the filter input. Hiding also clears the active filter, so
57
49
  * the user never returns to an "invisible" filter narrowing the rows. */
58
50
  private _toggleFilterBar;
@@ -157,7 +149,7 @@ export declare class AssistantSessionsPanel extends Widget {
157
149
  /** The server mints the new id and answers with it before the terminal
158
150
  * opens - `server-copy` copies the store entry, `server-native` drives the
159
151
  * assistant's own non-interactive fork path - so one refresh surfaces the
160
- * branch. */
152
+ * branch. `spinner` is the click's token, which the launch joins. */
161
153
  private _branchByServerCopy;
162
154
  /** Drop the hand-set colours of this project's conversations and re-tint
163
155
  * from the defaults at once, so nothing keeps showing a released colour
@@ -310,11 +302,6 @@ export declare class AssistantSessionsPanel extends Widget {
310
302
  private readonly _removingPaths;
311
303
  private _bodyEl;
312
304
  private _loadingEl;
313
- private _busyEl;
314
- private _busyLabelEl;
315
- private _busyTextEl;
316
- private _busySeq;
317
- private _busyMessages;
318
305
  private _errorEl;
319
306
  private _refreshBtn;
320
307
  private _filterBtn;
package/lib/core/panel.js CHANGED
@@ -5,6 +5,7 @@ import { CommandRegistry } from '@lumino/commands';
5
5
  import { UUID } from '@lumino/coreutils';
6
6
  import { Widget } from '@lumino/widgets';
7
7
  import { ColourStore } from './colour';
8
+ import { dialogRenderer } from './dialogs';
8
9
  import { branchMenuLabel, shortSessionId } from './labels';
9
10
  import { addIcon, branchIcon, cleanupIcon, filterIcon, providerIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon, statusDotIcon, switchIcon } from './icons';
10
11
  import { DEFAULT_RECENT_LIMIT, MAX_RECENT_LIMIT, MIN_RECENT_LIMIT } from './limits';
@@ -12,7 +13,7 @@ import { resolveLaunchMode, resolvedLaunchModeEntry } from './modes';
12
13
  import { showManageSessionsPopup } from './popup';
13
14
  import { LOG_PREFIX } from './registry';
14
15
  import { isRequestTimeout, isResponseStatus, requestProvider, withQuery } from './request';
15
- import { TerminalManager } from './terminals';
16
+ import { TerminalManager, beginSpinner, endSpinner } from './terminals';
16
17
  // Rows refresh on this cadence. It is also what refreshes whatever roster the
17
18
  // server caches for the context menu, so raising it past the server's cache
18
19
  // horizon leaves that snapshot stale at every menu open.
@@ -112,11 +113,6 @@ export class AssistantSessionsPanel extends Widget {
112
113
  this._deleteToTrash = true;
113
114
  this._removingPaths = new Set();
114
115
  this._loadingEl = null;
115
- this._busyEl = null;
116
- this._busyLabelEl = null;
117
- this._busyTextEl = null;
118
- this._busySeq = 0;
119
- this._busyMessages = new Map();
120
116
  this._errorEl = null;
121
117
  this._refreshBtn = null;
122
118
  this._filterBtn = null;
@@ -188,13 +184,7 @@ export class AssistantSessionsPanel extends Widget {
188
184
  return this._descriptor;
189
185
  }
190
186
  dispose() {
191
- var _a;
192
187
  this._stopPolling();
193
- // The busy overlay lives on the shell, outside this widget's tree, so
194
- // removing the panel does not remove it. Left behind it would show a
195
- // spinner for a panel that no longer exists.
196
- (_a = this._busyEl) === null || _a === void 0 ? void 0 : _a.remove();
197
- this._busyEl = null;
198
188
  // Disabling a provider must leave its running terminals alive and only
199
189
  // drop the tint this panel applied.
200
190
  void this._terminals.clearColours();
@@ -400,39 +390,6 @@ export class AssistantSessionsPanel extends Widget {
400
390
  loadingSpinner.className =
401
391
  'jp-aiAssistants-spinner jp-AiAssistantsPanel-loadingSpinner';
402
392
  loading.appendChild(loadingSpinner);
403
- // Click-time busy overlay: raised synchronously when a row is opened or
404
- // branched, before the terminal probe or the branch request, so the click
405
- // never reads as ignored (DEF-PANE-318). It sits on the LAB SHELL, not on
406
- // the panel: the feedback belongs in the main working area, where the
407
- // launch spinner appears, and a panel that is hidden or scrolled must not
408
- // keep it out of sight (DEF-PANE-318-1). It takes no pointer events and no
409
- // focus: the context menu that launched a branch stays open until its
410
- // command runs, and a focus change would close it.
411
- const busy = document.createElement('div');
412
- busy.className = 'jp-AiAssistants-busyOverlay';
413
- busy.hidden = true;
414
- const busySpinner = document.createElement('div');
415
- busySpinner.className =
416
- 'jp-aiAssistants-spinner jp-AiAssistants-busySpinner';
417
- busySpinner.setAttribute('aria-hidden', 'true');
418
- busy.appendChild(busySpinner);
419
- // The action, named in sight: under a reduced-motion preference the ring
420
- // is held still, so the label carries the busy state with no animation.
421
- // Hidden from assistive technology because the live region below
422
- // announces the same words once.
423
- const busyLabel = document.createElement('div');
424
- busyLabel.className = 'jp-AiAssistants-busyLabel';
425
- busyLabel.setAttribute('aria-hidden', 'true');
426
- busy.appendChild(busyLabel);
427
- // The spinner is decoration; a screen-reader user hears the action
428
- // instead. The region sits on the root, always rendered and outside the
429
- // overlay and the aria-busy body: a live region created inside a
430
- // display:none container, or one whose update lands in an aria-busy
431
- // subtree, can be withheld by assistive technology.
432
- const busyText = document.createElement('div');
433
- busyText.className = 'jp-AiAssistantsPanel-srOnly';
434
- busyText.setAttribute('role', 'status');
435
- busyText.setAttribute('aria-live', 'polite');
436
393
  // Inline error strip - a failed poll reports here and keeps retrying,
437
394
  // rather than tearing the panel down or raising a modal that would also
438
395
  // interrupt the other providers' panels.
@@ -447,69 +404,10 @@ export class AssistantSessionsPanel extends Widget {
447
404
  root.appendChild(error);
448
405
  root.appendChild(body);
449
406
  root.appendChild(loading);
450
- root.appendChild(busyText);
451
- // The shell carries the visible feedback; the panel carries the
452
- // announcement.
453
- this._app.shell.node.appendChild(busy);
454
407
  this._bodyEl = body;
455
408
  this._loadingEl = loading;
456
- this._busyEl = busy;
457
- this._busyLabelEl = busyLabel;
458
- this._busyTextEl = busyText;
459
409
  this._errorEl = error;
460
410
  }
461
- /** Raise the click-time busy overlay. Each action gets a token, because an
462
- * open and a branch can overlap; the overlay clears when the last one ends,
463
- * and the label names the newest action still running. */
464
- _beginBusy(message) {
465
- const token = ++this._busySeq;
466
- this._busyMessages.set(token, message);
467
- if (this._busyEl) {
468
- this._busyEl.hidden = false;
469
- this._paintBusyMessage();
470
- // The body is what the action changes; the root stays outside the
471
- // busy subtree so the live region above is not withheld.
472
- if (this._bodyEl) {
473
- this._bodyEl.setAttribute('aria-busy', 'true');
474
- }
475
- }
476
- return token;
477
- }
478
- _endBusy(token) {
479
- this._busyMessages.delete(token);
480
- if (this._busyMessages.size > 0) {
481
- // Another action is still running; neither the visible label nor the
482
- // live region may keep naming the one that just ended.
483
- this._paintBusyMessage();
484
- return;
485
- }
486
- if (this._busyEl) {
487
- this._busyEl.hidden = true;
488
- if (this._busyLabelEl) {
489
- this._busyLabelEl.textContent = '';
490
- }
491
- if (this._busyTextEl) {
492
- this._busyTextEl.textContent = '';
493
- }
494
- if (this._bodyEl) {
495
- this._bodyEl.removeAttribute('aria-busy');
496
- }
497
- }
498
- }
499
- /** Name the newest still-running action in the visible label and the live
500
- * region, so neither keeps naming an action that ended. */
501
- _paintBusyMessage() {
502
- let message = '';
503
- for (const active of this._busyMessages.values()) {
504
- message = active;
505
- }
506
- if (this._busyLabelEl) {
507
- this._busyLabelEl.textContent = message;
508
- }
509
- if (this._busyTextEl) {
510
- this._busyTextEl.textContent = message;
511
- }
512
- }
513
411
  /** Show / hide the filter input. Hiding also clears the active filter, so
514
412
  * the user never returns to an "invisible" filter narrowing the rows. */
515
413
  _toggleFilterBar() {
@@ -739,7 +637,8 @@ export class AssistantSessionsPanel extends Widget {
739
637
  // to Remove - one Enter on a dialog that has just appeared and the
740
638
  // project history is gone (DEF-51). JupyterLab's own file browser sets
741
639
  // the same 0 on its delete, for the same reason.
742
- defaultButton: 0
640
+ defaultButton: 0,
641
+ renderer: dialogRenderer
743
642
  });
744
643
  if (!confirm.button.accept) {
745
644
  return;
@@ -776,7 +675,8 @@ export class AssistantSessionsPanel extends Widget {
776
675
  Dialog.warnButton({ label: this._trans.__('Remove') })
777
676
  ],
778
677
  // Cancel, as above (DEF-51).
779
- defaultButton: 0
678
+ defaultButton: 0,
679
+ renderer: dialogRenderer
780
680
  });
781
681
  if (!confirm.button.accept) {
782
682
  return;
@@ -796,7 +696,8 @@ export class AssistantSessionsPanel extends Widget {
796
696
  const dialog = new Dialog({
797
697
  title: this._trans.__('Clean Up Parallel Sessions'),
798
698
  body,
799
- buttons: [Dialog.okButton({ label: this._trans.__('Close') })]
699
+ buttons: [Dialog.okButton({ label: this._trans.__('Close') })],
700
+ renderer: dialogRenderer
800
701
  });
801
702
  // Close stays live while the work runs. Hiding it took away the only exit
802
703
  // a keyboard user has: the dialog listens for Escape on its own node, it
@@ -854,10 +755,11 @@ export class AssistantSessionsPanel extends Widget {
854
755
  }
855
756
  async _openSession(session, force, sessionId) {
856
757
  // The click's own feedback, raised before the terminal walk and the
857
- // launch - both can take seconds (DEF-PANE-318).
858
- const busy = this._beginBusy(this._trans.__('Opening conversation'));
758
+ // launch - both can take seconds (DEF-PANE-318). The launch's title; the
759
+ // launch joins this dialog through the token.
760
+ const busy = beginSpinner(this._trans.__('Opening %1', this._descriptor.label), this._trans);
859
761
  try {
860
- await this._terminals.openSession(session, this._launchMode(force), sessionId);
762
+ await this._terminals.openSession(session, this._launchMode(force), sessionId, busy);
861
763
  // Not awaited: the open has succeeded, and the sync is not part of it.
862
764
  fileBrowserMoves = fileBrowserMoves.then(() => this._moveFileBrowserTo(session));
863
765
  }
@@ -870,7 +772,7 @@ export class AssistantSessionsPanel extends Widget {
870
772
  }
871
773
  }
872
774
  finally {
873
- this._endBusy(busy);
775
+ endSpinner(busy);
874
776
  // Reuse or fresh launch, either way the picture changed. Pull fresh
875
777
  // state; a poll tick retries if this one fails.
876
778
  void this._fetch().catch(() => undefined);
@@ -1101,20 +1003,21 @@ export class AssistantSessionsPanel extends Widget {
1101
1003
  }
1102
1004
  // Raised on the click: the branch POST mints the fork server-side (for
1103
1005
  // Claude and Codex by running their own CLI) and can take seconds
1104
- // (DEF-PANE-318).
1105
- const busy = this._beginBusy(this._trans.__('Branching conversation'));
1006
+ // (DEF-PANE-318). The menu that ran this command closed before running it,
1007
+ // so the dialog taking the focus closes nothing.
1008
+ const busy = beginSpinner(this._trans.__('Branching %1 session', this._descriptor.label), this._trans);
1106
1009
  try {
1107
- await this._branchByServerCopy(session, undefined, force);
1010
+ await this._branchByServerCopy(session, undefined, force, busy);
1108
1011
  }
1109
1012
  finally {
1110
- this._endBusy(busy);
1013
+ endSpinner(busy);
1111
1014
  }
1112
1015
  }
1113
1016
  /** The server mints the new id and answers with it before the terminal
1114
1017
  * opens - `server-copy` copies the store entry, `server-native` drives the
1115
1018
  * assistant's own non-interactive fork path - so one refresh surfaces the
1116
- * branch. */
1117
- async _branchByServerCopy(session, name, force) {
1019
+ * branch. `spinner` is the click's token, which the launch joins. */
1020
+ async _branchByServerCopy(session, name, force, spinner) {
1118
1021
  // Let the parent's colour settle before the server reads it (DEF-74). The
1119
1022
  // server copies the store entry while minting the fork, so a colour picked
1120
1023
  // on the parent's tab moments earlier - still queued in this store's
@@ -1151,13 +1054,10 @@ export class AssistantSessionsPanel extends Widget {
1151
1054
  project_path: session.project_path,
1152
1055
  session_id: fork.session_id,
1153
1056
  mode: this._launchMode(force)
1154
- }, fork.session_id);
1057
+ }, fork.session_id, spinner);
1155
1058
  }
1156
1059
  catch (err) {
1157
- if (isResponseStatus(err, 400)) {
1158
- Notification.error(`${this._descriptor.label} cannot branch this conversation.`, { autoClose: 4000 });
1159
- }
1160
- else if (minted) {
1060
+ if (minted) {
1161
1061
  // The branch was created and only its terminal was refused - the
1162
1062
  // launch preflight's 409 carries words for the user. "Could not
1163
1063
  // branch this session" would be false, and the early return would
@@ -1170,6 +1070,9 @@ export class AssistantSessionsPanel extends Widget {
1170
1070
  this._notifyLaunchError('Branch created; its terminal did not open', err);
1171
1071
  await this._fetch().catch(() => undefined);
1172
1072
  }
1073
+ else if (isResponseStatus(err, 400)) {
1074
+ Notification.error(`${this._descriptor.label} cannot branch this conversation.`, { autoClose: 4000 });
1075
+ }
1173
1076
  else {
1174
1077
  this._showActionError('Could not branch this session - try again.', err);
1175
1078
  }
@@ -1217,7 +1120,8 @@ export class AssistantSessionsPanel extends Widget {
1217
1120
  title: 'Rename Session',
1218
1121
  label: `Name for this ${this._descriptor.label} conversation`,
1219
1122
  text: session.name_source === 'session' ? session.name : '',
1220
- placeholder: this._lookupName(session)
1123
+ placeholder: this._lookupName(session),
1124
+ renderer: dialogRenderer
1221
1125
  });
1222
1126
  if (!named.button.accept) {
1223
1127
  return;
package/lib/core/popup.js CHANGED
@@ -2,6 +2,7 @@ import { Clipboard, Dialog } from '@jupyterlab/apputils';
2
2
  import { nullTranslator } from '@jupyterlab/translation';
3
3
  import { copyIcon } from '@jupyterlab/ui-components';
4
4
  import { Widget } from '@lumino/widgets';
5
+ import { DialogRenderer } from './dialogs';
5
6
  import { branchIcon } from './icons';
6
7
  /** Compact copy button for a popup row. `stopPropagation` keeps the click from
7
8
  * switching or selecting the row it sits in. */
@@ -17,14 +18,14 @@ function copyButton(sessionId, trans) {
17
18
  });
18
19
  return btn;
19
20
  }
20
- /** The default dialog renderer with one extra node leading the footer.
21
+ /** The extension's dialog renderer with one extra node leading the footer.
21
22
  * JupyterLab builds the footer in the Dialog constructor through
22
23
  * `renderer.createFooter`, and `IOptions.renderer` is the public hook for
23
24
  * changing it; its only built-in extra, `checkbox`, renders a checkbox, not a
24
25
  * button. Leading the footer puts the node before Close in tab order, so the
25
26
  * keyboard reaches the list, then Delete, then Close - and the Dialog's own Tab
26
27
  * wrap, keyed on its last button, still wraps from Close. */
27
- class FooterRenderer extends Dialog.Renderer {
28
+ class FooterRenderer extends DialogRenderer {
28
29
  constructor(_lead) {
29
30
  super();
30
31
  this._lead = _lead;
@@ -49,6 +50,9 @@ export function showManageSessionsPopup(options) {
49
50
  // removal is permanent.
50
51
  let armed = false;
51
52
  let armTimer = 0;
53
+ // The arming click's event.timeStamp, which the confirm click is measured
54
+ // against.
55
+ let armedAt = 0;
52
56
  const body = document.createElement('div');
53
57
  body.className = 'jp-AiAssistantsPanel-branchPopup';
54
58
  const search = document.createElement('input');
@@ -347,10 +351,17 @@ export function showManageSessionsPopup(options) {
347
351
  list.removeAttribute('aria-busy');
348
352
  }
349
353
  };
350
- deleteBtn.addEventListener('click', () => {
354
+ deleteBtn.addEventListener('click', event => {
351
355
  if (deleting || selected.size === 0) {
352
356
  return;
353
357
  }
358
+ // The second click of a double-click is not a confirmation (DEF-PANE-336):
359
+ // a multi-click within 600 ms of arming is ignored. A later click confirms
360
+ // whatever its detail - the OS double-click interval can be longer than
361
+ // the user's pause, and the click would be dropped (DEF-PANE-338).
362
+ if (armed && event.detail > 1 && event.timeStamp - armedAt < 600) {
363
+ return;
364
+ }
354
365
  // No confirmation DIALOG here: a second Lumino dialog stacked on this popup
355
366
  // renders detached. The deletion confirms in the footer itself - the first
356
367
  // click arms the button, the second executes, and any selection change or
@@ -359,6 +370,7 @@ export function showManageSessionsPopup(options) {
359
370
  // refused move may remove the conversation permanently or leave it.
360
371
  if (!armed) {
361
372
  armed = true;
373
+ armedAt = event.timeStamp;
362
374
  window.clearTimeout(armTimer);
363
375
  armTimer = window.setTimeout(() => {
364
376
  disarm();
@@ -1,26 +1,29 @@
1
1
  import { JupyterFrontEnd } from '@jupyterlab/application';
2
- import { Dialog } from '@jupyterlab/apputils';
3
2
  import { TranslationBundle } from '@jupyterlab/translation';
4
3
  import { ServerConnection } from '@jupyterlab/services';
5
4
  import { ITerminalTracker } from '@jupyterlab/terminal';
6
5
  import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
7
6
  import { ColourStore } from './colour';
8
7
  import { ILaunchRequest, IProviderDescriptor, ISession, ITerminalProbeResponse } from './types';
9
- /** Show a modal spinner while a terminal is being launched. The caller
10
- * dismisses it with `.dispose()` when the launch settles.
11
- *
12
- * Hide does not abort the request - it only takes the app-wide scrim away, so
13
- * a stalled launch cannot leave the user staring at a modal with no exit. If
14
- * the launch later succeeds the terminal simply appears. */
15
- export declare function showLaunchSpinner(title: string, trans: TranslationBundle): Dialog<unknown>;
8
+ /** Show the spinner dialog for one action, or retitle the one already up so it
9
+ * names the newest action. Pass the token to `endSpinner` when the action
10
+ * settles. Hide disposes the dialog for the actions then running: they stay
11
+ * hidden and keep no dialog up, and the next action raises a fresh one. The
12
+ * launch inside a click passes the click's token to `launch()`, so a hidden
13
+ * click's launch raises nothing. */
14
+ export declare function beginSpinner(title: string, trans: TranslationBundle): number;
15
+ /** End one action. The dialog then names the newest action still running, or
16
+ * closes with the last one. */
17
+ export declare function endSpinner(token: number): void;
16
18
  /**
17
19
  * Terminals for one provider: the reuse ladder, the launch call and the tab
18
20
  * tint pass.
19
21
  *
20
22
  * Split out of the panel because none of it is presentation - it is process
21
- * identity. The panel asks for a conversation and gets a focused terminal
22
- * running exactly that conversation, or a fresh one; nothing in here knows
23
- * about sections, filters or menus.
23
+ * identity. The panel asks for a conversation and gets a terminal running
24
+ * exactly that conversation, or a fresh one, focused unless the user hid the
25
+ * action's spinner dialog; nothing in here knows about sections, filters or
26
+ * menus.
24
27
  */
25
28
  export declare class TerminalManager {
26
29
  constructor(options: TerminalManager.IOptions);
@@ -40,22 +43,42 @@ export declare class TerminalManager {
40
43
  * promise; a different conversation of the same project launches
41
44
  * independently, so branches open side by side.
42
45
  */
43
- openSession(session: ISession, mode: string | undefined, sessionId?: string): Promise<void>;
46
+ openSession(session: ISession, mode: string | undefined, sessionId?: string, spinner?: number): Promise<void>;
44
47
  private _doOpenSession;
45
48
  /**
46
49
  * Launch a terminal for one request and focus it. The returned widget is
47
50
  * tagged with `tagSessionId` in the microcache, so a later click on the row
48
51
  * that conversation now belongs to reuses this very terminal.
52
+ *
53
+ * `spinner` is the token of the click this launch belongs to. The launch
54
+ * joins that click's spinner instead of beginning its own, so the user sees
55
+ * one dialog from the click to the terminal, and a click the user hid stays
56
+ * hidden and its terminal opens behind the current tab.
49
57
  */
50
- launch(request: ILaunchRequest, tagSessionId?: string): Promise<any>;
58
+ launch(request: ILaunchRequest, tagSessionId?: string, spinner?: number): Promise<any>;
59
+ /** Show the current widget's tab again. Opening a terminal selects its tab,
60
+ * so for a hidden action the tab the user was on goes back in front
61
+ * (DEF-PANE-339). */
62
+ private _showCurrentTab;
51
63
  /**
52
64
  * Bring a terminal tab to the front AND hand it keyboard focus, so typing
53
65
  * starts without a second click. `activateById` only reveals the tab; the
54
66
  * xterm inside does not reliably take DOM focus when the click came from the
55
67
  * sidebar, so the focus call is deferred to the next frame - by then the
56
68
  * widget is attached and visible.
69
+ *
70
+ * The activation waits for the spinner dialog to close. While the dialog is
71
+ * up it pulls any focus outside it back to Hide, and on closing it returns
72
+ * the focus to the element that had it before - so a terminal activated
73
+ * under it ends up without the keyboard. Closing emits `disposed` before
74
+ * that return, and the activation the tab then receives is processed after
75
+ * it, so the terminal is the last to take the focus.
76
+ *
77
+ * `spinner` is the token of the action that asked. When the user hid that
78
+ * action's dialog the terminal is left where it is, and so is a terminal
79
+ * waiting for the dialog the user then hides (DEF-PANE-339).
57
80
  */
58
- focus(widget: any): void;
81
+ focus(widget: any, spinner?: number): void;
59
82
  /** Find an open terminal running exactly this conversation in this project,
60
83
  * or null.
61
84
  *