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 +1 -1
- package/lib/core/callback.js +3 -1
- package/lib/core/dialogs.d.ts +18 -0
- package/lib/core/dialogs.js +21 -0
- package/lib/core/icons.d.ts +14 -2
- package/lib/core/icons.js +22 -4
- package/lib/core/panel.d.ts +1 -14
- package/lib/core/panel.js +27 -123
- package/lib/core/popup.js +15 -3
- package/lib/core/terminals.d.ts +37 -14
- package/lib/core/terminals.js +180 -26
- package/lib/providers/claude.js +3 -2
- package/lib/providers/codex.js +3 -5
- package/lib/providers/deepseek.js +3 -3
- package/lib/providers/gemini.js +3 -3
- package/lib/providers/kimi.js +3 -2
- package/package.json +4 -1
- package/schema/plugin.json +1 -2
- package/src/__tests__/icon-size.spec.ts +277 -0
- package/src/__tests__/index.spec.ts +6 -14
- package/src/__tests__/launcher-icon.spec.ts +9 -3
- package/src/__tests__/launcher.spec.ts +2 -6
- package/src/__tests__/panel.spec.ts +179 -232
- package/src/__tests__/popup.spec.ts +47 -0
- package/src/__tests__/terminals.spec.ts +288 -1
- package/src/core/callback.ts +3 -1
- package/src/core/dialogs.ts +27 -0
- package/src/core/icons.ts +25 -4
- package/src/core/panel.ts +38 -128
- package/src/core/popup.ts +15 -3
- package/src/core/terminals.ts +207 -32
- package/src/providers/claude.ts +3 -2
- package/src/providers/codex.ts +3 -5
- package/src/providers/deepseek.ts +3 -3
- package/src/providers/gemini.ts +3 -3
- package/src/providers/kimi.ts +3 -2
- package/style/base.css +86 -37
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
|
|
package/lib/core/callback.js
CHANGED
|
@@ -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();
|
package/lib/core/icons.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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, {
|
|
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;
|
package/lib/core/panel.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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();
|
package/lib/core/terminals.d.ts
CHANGED
|
@@ -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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
export declare function
|
|
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
|
|
22
|
-
*
|
|
23
|
-
* about sections, filters or
|
|
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
|
*
|