jupyterlab_ai_code_assistants_extension 1.2.120 → 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 +2 -2
- package/lib/core/callback.js +3 -1
- package/lib/core/dialogs.d.ts +16 -0
- package/lib/core/dialogs.js +19 -0
- package/lib/core/icons.d.ts +9 -1
- package/lib/core/icons.js +9 -1
- package/lib/core/panel.d.ts +1 -14
- package/lib/core/panel.js +33 -122
- package/lib/core/popup.d.ts +4 -0
- package/lib/core/popup.js +40 -23
- package/lib/core/terminals.d.ts +24 -10
- package/lib/core/terminals.js +100 -13
- 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 +1 -1
- package/schema/plugin.json +1 -2
- package/src/__tests__/icon-size.spec.ts +275 -0
- package/src/__tests__/index.spec.ts +6 -14
- package/src/__tests__/launcher.spec.ts +2 -6
- package/src/__tests__/panel.spec.ts +130 -166
- package/src/__tests__/popup.spec.ts +75 -2
- package/src/__tests__/terminals.spec.ts +172 -1
- package/src/core/callback.ts +3 -1
- package/src/core/dialogs.ts +25 -0
- package/src/core/icons.ts +9 -1
- package/src/core/panel.ts +43 -125
- package/src/core/popup.ts +44 -23
- package/src/core/terminals.ts +120 -18
- 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 +73 -37
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
33
33
|
- **File browser follows the opened session** - opening a session moves the JupyterLab file browser to that session's project folder (a setting, on by default); a right-click on a panel header toggles this and moves the panels between the left and right sidebars
|
|
34
34
|
- **Launcher tiles** - every enabled assistant has a tile in an "AI Assistants" section of the JupyterLab Launcher (after Other, under the extension's own robot-head icon); a click opens the assistant in the file browser's current folder, resuming the folder's conversation when one exists and starting a new one otherwise, through the companion `jupyterlab_basic_terminal_extension` (installed automatically). Disable an assistant and its tile goes; disable them all and the section goes with them
|
|
35
35
|
- **Your terminal's `claude -c` follows the panel** (Claude) - switching to a conversation makes plain `claude -c` in that project resume that same conversation, including one that has been compacted, which Claude Code otherwise refuses to continue. Launching repairs a compacted conversation the same way, but only switching moves what `claude -c` picks
|
|
36
|
-
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id with last-activity time; "Manage Sessions..." opens a searchable popup over the full list with multi-select delete
|
|
36
|
+
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id with last-activity time; "Manage Sessions..." opens a searchable popup over the full list with multi-select delete, a per-row action that switches or opens according to the submenu it was opened from, and a copy-id button
|
|
37
37
|
- **Branch session** - fork the current conversation into a new named session via the right-click menu; each assistant forks its own way (Claude's native `--fork-session`, Codex's app-server `thread/fork`, server-side copies for Kimi, Gemini and DeepSeek) behind the same menu item
|
|
38
38
|
- **Rename session** - name a conversation from the right-click menu, and the name is written where the assistant itself reads it back: Claude gets the record its own `/rename` writes, Kimi a custom title, Gemini the chat's summary, DeepSeek its title event. The panel shows the name the assistant actually kept. Codex's name is set through Codex's own app-server, so its CLI lists the same name the panel shows
|
|
39
39
|
- **Launch modes under each assistant's own name** - Claude's skip-permissions, Codex's approval bypass, Kimi's `--yolo`, Gemini's YOLO; unsafe variants carry a shield glyph in the launch menus
|
|
@@ -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,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();
|
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.
|
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
|
-
/**
|
|
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) {
|
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,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;
|
|
@@ -2083,7 +1987,13 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2083
1987
|
});
|
|
2084
1988
|
this._commands.addCommand(this._cmd('manage-sessions'), {
|
|
2085
1989
|
label: () => `Manage Sessions... (${this._lastBranches.length})`,
|
|
2086
|
-
execute: () => this._openManagePopup()
|
|
1990
|
+
execute: () => this._openManagePopup('switch')
|
|
1991
|
+
});
|
|
1992
|
+
// The open submenu's copy of the item: same popup, but a row action
|
|
1993
|
+
// launches the conversation rather than making it current.
|
|
1994
|
+
this._commands.addCommand(this._cmd('manage-sessions-open'), {
|
|
1995
|
+
label: () => `Manage Sessions... (${this._lastBranches.length})`,
|
|
1996
|
+
execute: () => this._openManagePopup('open')
|
|
2087
1997
|
});
|
|
2088
1998
|
this._commands.addCommand(this._cmd('open-branch'), {
|
|
2089
1999
|
// Names the mode the launch carries, like Resume, `+` and Branch Session
|
|
@@ -2417,10 +2327,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2417
2327
|
}
|
|
2418
2328
|
this._openBranchSubmenu.addItem({ type: 'separator' });
|
|
2419
2329
|
this._openBranchSubmenu.addItem({
|
|
2420
|
-
command: this._cmd('manage-sessions')
|
|
2330
|
+
command: this._cmd('manage-sessions-open')
|
|
2421
2331
|
});
|
|
2422
2332
|
}
|
|
2423
|
-
_openManagePopup() {
|
|
2333
|
+
_openManagePopup(mode) {
|
|
2424
2334
|
const session = this._activeSession;
|
|
2425
2335
|
if (!session) {
|
|
2426
2336
|
return;
|
|
@@ -2428,6 +2338,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2428
2338
|
const current = this._lastBranchesCurrent || session.session_id;
|
|
2429
2339
|
showManageSessionsPopup({
|
|
2430
2340
|
trans: this._trans,
|
|
2341
|
+
mode,
|
|
2431
2342
|
branches: this._lastBranches,
|
|
2432
2343
|
current,
|
|
2433
2344
|
currentName: `${this._lookupName(session)} (${shortSessionId(current, this._descriptor.sessionIdPrefix)})`,
|
package/lib/core/popup.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ export declare namespace ManageSessionsPopup {
|
|
|
29
29
|
formatTime: (epochMs: number) => string;
|
|
30
30
|
/** Provider chip for a branch row, or null. */
|
|
31
31
|
branchBadge?: (branch: IBranch) => HTMLElement | null;
|
|
32
|
+
/** What a row's action does, inherited from the submenu that opened the
|
|
33
|
+
* popup: `switch` makes the conversation current, `open` launches it in
|
|
34
|
+
* its own terminal. */
|
|
35
|
+
mode: 'switch' | 'open';
|
|
32
36
|
/** Tooltip for one row's Open button. The panel owns it because the launch
|
|
33
37
|
* mode in force is panel state, and every other surface that opens a
|
|
34
38
|
* conversation names it. */
|
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;
|
|
@@ -115,19 +116,32 @@ export function showManageSessionsPopup(options) {
|
|
|
115
116
|
});
|
|
116
117
|
// Scopes the action-row layout in base.css to this one dialog.
|
|
117
118
|
dialog.addClass('jp-AiAssistantsPanel-branchDialog');
|
|
118
|
-
//
|
|
119
|
-
// the
|
|
120
|
-
//
|
|
121
|
-
const
|
|
119
|
+
// The row action follows the mode the invoking submenu passed: switch makes
|
|
120
|
+
// the conversation current, open launches it in its own terminal. Both close
|
|
121
|
+
// the popup first - the action is complete once it runs.
|
|
122
|
+
const runAction = (sessionId) => {
|
|
123
|
+
dialog.dispose();
|
|
124
|
+
if (options.mode === 'switch') {
|
|
125
|
+
options.onSwitch(sessionId);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
options.onOpen(sessionId);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
// Per-row action button. stopPropagation keeps the click from toggling
|
|
132
|
+
// selection or repeating the action through the row's own click handler.
|
|
133
|
+
const actionButton = (sessionId) => {
|
|
122
134
|
const btn = document.createElement('button');
|
|
123
135
|
btn.type = 'button';
|
|
124
136
|
btn.className = 'jp-AiAssistantsPanel-branchOpen';
|
|
125
|
-
btn.textContent = 'Open';
|
|
126
|
-
btn.title =
|
|
137
|
+
btn.textContent = options.mode === 'switch' ? 'Switch' : 'Open';
|
|
138
|
+
btn.title =
|
|
139
|
+
options.mode === 'switch'
|
|
140
|
+
? "Make this the project's current conversation"
|
|
141
|
+
: options.openTitle();
|
|
127
142
|
btn.addEventListener('click', e => {
|
|
128
143
|
e.stopPropagation();
|
|
129
|
-
|
|
130
|
-
options.onOpen(sessionId);
|
|
144
|
+
runAction(sessionId);
|
|
131
145
|
});
|
|
132
146
|
return btn;
|
|
133
147
|
};
|
|
@@ -192,7 +206,11 @@ export function showManageSessionsPopup(options) {
|
|
|
192
206
|
badge.className = 'jp-AiAssistantsPanel-branchCurrentBadge';
|
|
193
207
|
badge.textContent = 'current';
|
|
194
208
|
currentRow.appendChild(badge);
|
|
195
|
-
|
|
209
|
+
// Switch mode has nothing to switch to on the row that is already current;
|
|
210
|
+
// open mode can still launch it in a terminal.
|
|
211
|
+
if (options.mode === 'open') {
|
|
212
|
+
currentRow.appendChild(actionButton(options.current));
|
|
213
|
+
}
|
|
196
214
|
currentRow.appendChild(copyButton(options.current, trans));
|
|
197
215
|
list.appendChild(currentRow);
|
|
198
216
|
const matches = visibleMatches();
|
|
@@ -270,20 +288,19 @@ export function showManageSessionsPopup(options) {
|
|
|
270
288
|
time.className = 'jp-AiAssistantsPanel-branchTime';
|
|
271
289
|
time.textContent = options.formatTime(b.file_mtime);
|
|
272
290
|
row.appendChild(time);
|
|
273
|
-
row.appendChild(
|
|
291
|
+
row.appendChild(actionButton(b.session_id));
|
|
274
292
|
row.appendChild(copyButton(b.session_id, trans));
|
|
275
|
-
// A row click
|
|
276
|
-
// is the one select target, so a click has one meaning
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
options.onSwitch(b.session_id);
|
|
280
|
-
};
|
|
293
|
+
// A row click follows the popup's mode, whatever is selected: the
|
|
294
|
+
// checkbox cell is the one select target, so a click has one meaning
|
|
295
|
+
// (DEF-PANE-209).
|
|
296
|
+
const activate = () => runAction(b.session_id);
|
|
281
297
|
row.addEventListener('click', activate);
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
// not the row: the row also hosts a checkbox and the
|
|
285
|
-
// and a role="button" wrapping them both swallows
|
|
286
|
-
// and makes them presentational children (ARIA
|
|
298
|
+
// The row action also has a button; the label keeps a role and key route
|
|
299
|
+
// so the row itself stays keyboard-reachable. The affordance sits on the
|
|
300
|
+
// label span, not the row: the row also hosts a checkbox and the
|
|
301
|
+
// action/Copy buttons, and a role="button" wrapping them both swallows
|
|
302
|
+
// their own Enter/Space and makes them presentational children (ARIA
|
|
303
|
+
// nested-interactive).
|
|
287
304
|
label.setAttribute('role', 'button');
|
|
288
305
|
label.tabIndex = 0;
|
|
289
306
|
label.addEventListener('keydown', e => {
|
package/lib/core/terminals.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
@@ -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,
|