jupyterlab_ai_code_assistants_extension 1.2.123 → 1.2.163

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,16 @@
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 any
6
+ * button, the reject buttons (Cancel, and Close in the Open Callback popup) as
7
+ * secondary surface buttons so the fill alone marks the primary - to a dialog
8
+ * that holds the mark, so JupyterLab's own
9
+ * dialogs keep their look. A renderer is the one hook that also reaches the
10
+ * dialogs JupyterLab builds on our behalf: `InputDialog.getText` passes its
11
+ * options through, and takes no class.
12
+ */
13
+ export declare class DialogRenderer extends Dialog.Renderer {
14
+ createFooter(buttons: ReadonlyArray<HTMLElement>, checkbox: HTMLElement | null): Widget;
15
+ }
16
+ export declare const dialogRenderer: DialogRenderer;
@@ -0,0 +1,19 @@
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 any
5
+ * button, the reject buttons (Cancel, and Close in the Open Callback popup) as
6
+ * secondary surface buttons so the fill alone marks the primary - to a dialog
7
+ * that holds the mark, so JupyterLab's own
8
+ * dialogs keep their look. A renderer is the one hook that also reaches the
9
+ * dialogs JupyterLab builds on our behalf: `InputDialog.getText` passes its
10
+ * options through, and takes no class.
11
+ */
12
+ export class DialogRenderer extends Dialog.Renderer {
13
+ createFooter(buttons, checkbox) {
14
+ const footer = super.createFooter(buttons, checkbox);
15
+ footer.addClass('jp-AiAssistantsPanel-dialogFooter');
16
+ return footer;
17
+ }
18
+ }
19
+ 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.
package/lib/core/icons.js CHANGED
@@ -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) {
@@ -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,7 +1054,7 @@ 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
1060
  if (isResponseStatus(err, 400)) {
@@ -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;
@@ -1,18 +1,20 @@
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.
@@ -40,20 +42,32 @@ export declare class TerminalManager {
40
42
  * promise; a different conversation of the same project launches
41
43
  * independently, so branches open side by side.
42
44
  */
43
- openSession(session: ISession, mode: string | undefined, sessionId?: string): Promise<void>;
45
+ openSession(session: ISession, mode: string | undefined, sessionId?: string, spinner?: number): Promise<void>;
44
46
  private _doOpenSession;
45
47
  /**
46
48
  * Launch a terminal for one request and focus it. The returned widget is
47
49
  * tagged with `tagSessionId` in the microcache, so a later click on the row
48
50
  * that conversation now belongs to reuses this very terminal.
51
+ *
52
+ * `spinner` is the token of the click this launch belongs to. The launch
53
+ * joins that click's spinner instead of beginning its own, so the user sees
54
+ * one dialog from the click to the terminal, and a click the user hid stays
55
+ * hidden.
49
56
  */
50
- launch(request: ILaunchRequest, tagSessionId?: string): Promise<any>;
57
+ launch(request: ILaunchRequest, tagSessionId?: string, spinner?: number): Promise<any>;
51
58
  /**
52
59
  * Bring a terminal tab to the front AND hand it keyboard focus, so typing
53
60
  * starts without a second click. `activateById` only reveals the tab; the
54
61
  * xterm inside does not reliably take DOM focus when the click came from the
55
62
  * sidebar, so the focus call is deferred to the next frame - by then the
56
63
  * widget is attached and visible.
64
+ *
65
+ * The activation waits for the spinner dialog to close. While the dialog is
66
+ * up it pulls any focus outside it back to Hide, and on closing it returns
67
+ * the focus to the element that had it before - so a terminal activated
68
+ * under it ends up without the keyboard. Closing emits `disposed` before
69
+ * that return, and the activation the tab then receives is processed after
70
+ * it, so the terminal is the last to take the focus.
57
71
  */
58
72
  focus(widget: any): void;
59
73
  /** Find an open terminal running exactly this conversation in this project,