jupyterlab_ai_code_assistants_extension 1.2.105 → 1.2.115
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 +17 -0
- package/lib/core/panel.d.ts +12 -6
- package/lib/core/panel.js +58 -19
- package/lib/core/popup.js +13 -13
- package/lib/index.js +3 -3
- package/package.json +1 -1
- package/src/__tests__/panel.spec.ts +32 -4
- package/src/__tests__/popup.spec.ts +15 -2
- package/src/core/panel.ts +60 -20
- package/src/core/popup.ts +13 -13
- package/src/index.ts +3 -3
- package/style/base.css +13 -1
package/README.md
CHANGED
|
@@ -76,6 +76,23 @@ pip install jupyterlab_ai_code_assistants_extension
|
|
|
76
76
|
> [!WARNING]
|
|
77
77
|
> `package.json` pins `webpack: 5.106.0` and `chalk: 4.1.2` in both `resolutions` and `overrides`. Do not remove these. webpack `>= 5.106.1` changed its module-federation share identifier format and crashes the unmaintained `license-webpack-plugin` (`split('=')[1].trim()`) that `@jupyterlab/builder` injects into every production build; the duplicate `chalk@2.4.2` pulled by `duplicate-package-checker-webpack-plugin` crashes on Node 24+ in the build-isolation install. Without the pins, `make publish` and CI fail on `python -m build`.
|
|
78
78
|
|
|
79
|
+
## Agent skill
|
|
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.
|
|
82
|
+
|
|
83
|
+
After `pip install`, with the Python that runs the lab:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
mkdir -p ~/.agents/skills
|
|
87
|
+
ln -s "$(python -c 'import sys; print(sys.prefix)')/share/jupyter/agents/skills/jupyterlab-ai-code-assistants-extension" ~/.agents/skills/jupyterlab-ai-code-assistants-extension
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
From a clone:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
ln -s "$PWD/.agents/skills/jupyterlab-ai-code-assistants-extension" ~/.claude/skills/jupyterlab-ai-code-assistants-extension
|
|
94
|
+
```
|
|
95
|
+
|
|
79
96
|
## Migrating from the standalone extensions
|
|
80
97
|
|
|
81
98
|
This package supersedes `jupyterlab_claude_code_extension`, `jupyterlab_codex_extension` and `jupyterlab_kimi_code_extension`. On first run it carries their settings and favourites over automatically, and it never replaces a setting you have already saved in this extension; uninstall the standalones once you are ready:
|
package/lib/core/panel.d.ts
CHANGED
|
@@ -45,10 +45,14 @@ 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.
|
|
49
|
-
* can overlap; the overlay clears when the last one ends
|
|
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. */
|
|
50
51
|
private _beginBusy;
|
|
51
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;
|
|
52
56
|
/** Show / hide the filter input. Hiding also clears the active filter, so
|
|
53
57
|
* the user never returns to an "invisible" filter narrowing the rows. */
|
|
54
58
|
private _toggleFilterBar;
|
|
@@ -90,9 +94,9 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
90
94
|
private _logError;
|
|
91
95
|
private _fetch;
|
|
92
96
|
private _toggleFavourite;
|
|
93
|
-
/** The warning a
|
|
94
|
-
*
|
|
95
|
-
*
|
|
97
|
+
/** The warning a deletion earns: with the trash setting on, a refused move
|
|
98
|
+
* may remove the conversations permanently or leave them in place, so
|
|
99
|
+
* neither the verb nor the note may promise one outcome. */
|
|
96
100
|
private get _irreversibleNote();
|
|
97
101
|
private get _disposalVerb();
|
|
98
102
|
private _removeProject;
|
|
@@ -307,8 +311,10 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
307
311
|
private _bodyEl;
|
|
308
312
|
private _loadingEl;
|
|
309
313
|
private _busyEl;
|
|
314
|
+
private _busyLabelEl;
|
|
310
315
|
private _busyTextEl;
|
|
311
|
-
private
|
|
316
|
+
private _busySeq;
|
|
317
|
+
private _busyMessages;
|
|
312
318
|
private _errorEl;
|
|
313
319
|
private _refreshBtn;
|
|
314
320
|
private _filterBtn;
|
package/lib/core/panel.js
CHANGED
|
@@ -113,8 +113,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
113
113
|
this._removingPaths = new Set();
|
|
114
114
|
this._loadingEl = null;
|
|
115
115
|
this._busyEl = null;
|
|
116
|
+
this._busyLabelEl = null;
|
|
116
117
|
this._busyTextEl = null;
|
|
117
|
-
this.
|
|
118
|
+
this._busySeq = 0;
|
|
119
|
+
this._busyMessages = new Map();
|
|
118
120
|
this._errorEl = null;
|
|
119
121
|
this._refreshBtn = null;
|
|
120
122
|
this._filterBtn = null;
|
|
@@ -414,6 +416,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
414
416
|
'jp-aiAssistants-spinner jp-AiAssistants-busySpinner';
|
|
415
417
|
busySpinner.setAttribute('aria-hidden', 'true');
|
|
416
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);
|
|
417
427
|
// The spinner is decoration; a screen-reader user hears the action
|
|
418
428
|
// instead. The region sits on the root, always rendered and outside the
|
|
419
429
|
// overlay and the aria-busy body: a live region created inside a
|
|
@@ -444,29 +454,40 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
444
454
|
this._bodyEl = body;
|
|
445
455
|
this._loadingEl = loading;
|
|
446
456
|
this._busyEl = busy;
|
|
457
|
+
this._busyLabelEl = busyLabel;
|
|
447
458
|
this._busyTextEl = busyText;
|
|
448
459
|
this._errorEl = error;
|
|
449
460
|
}
|
|
450
|
-
/** Raise the click-time busy overlay.
|
|
451
|
-
* can overlap; the overlay clears when the last one ends
|
|
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. */
|
|
452
464
|
_beginBusy(message) {
|
|
453
|
-
this.
|
|
465
|
+
const token = ++this._busySeq;
|
|
466
|
+
this._busyMessages.set(token, message);
|
|
454
467
|
if (this._busyEl) {
|
|
455
468
|
this._busyEl.hidden = false;
|
|
456
|
-
|
|
457
|
-
this._busyTextEl.textContent = message;
|
|
458
|
-
}
|
|
469
|
+
this._paintBusyMessage();
|
|
459
470
|
// The body is what the action changes; the root stays outside the
|
|
460
471
|
// busy subtree so the live region above is not withheld.
|
|
461
472
|
if (this._bodyEl) {
|
|
462
473
|
this._bodyEl.setAttribute('aria-busy', 'true');
|
|
463
474
|
}
|
|
464
475
|
}
|
|
476
|
+
return token;
|
|
465
477
|
}
|
|
466
|
-
_endBusy() {
|
|
467
|
-
this.
|
|
468
|
-
if (this.
|
|
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) {
|
|
469
487
|
this._busyEl.hidden = true;
|
|
488
|
+
if (this._busyLabelEl) {
|
|
489
|
+
this._busyLabelEl.textContent = '';
|
|
490
|
+
}
|
|
470
491
|
if (this._busyTextEl) {
|
|
471
492
|
this._busyTextEl.textContent = '';
|
|
472
493
|
}
|
|
@@ -475,6 +496,20 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
475
496
|
}
|
|
476
497
|
}
|
|
477
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
|
+
}
|
|
478
513
|
/** Show / hide the filter input. Hiding also clears the active filter, so
|
|
479
514
|
* the user never returns to an "invisible" filter narrowing the rows. */
|
|
480
515
|
_toggleFilterBar() {
|
|
@@ -674,14 +709,18 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
674
709
|
this._showActionError('Could not toggle favorite - try again.', err);
|
|
675
710
|
}
|
|
676
711
|
}
|
|
677
|
-
/** The warning a
|
|
678
|
-
*
|
|
679
|
-
*
|
|
712
|
+
/** The warning a deletion earns: with the trash setting on, a refused move
|
|
713
|
+
* may remove the conversations permanently or leave them in place, so
|
|
714
|
+
* neither the verb nor the note may promise one outcome. */
|
|
680
715
|
get _irreversibleNote() {
|
|
681
|
-
return this._deleteToTrash
|
|
716
|
+
return this._deleteToTrash
|
|
717
|
+
? ' A permanent removal cannot be undone.'
|
|
718
|
+
: ' This cannot be undone.';
|
|
682
719
|
}
|
|
683
720
|
get _disposalVerb() {
|
|
684
|
-
return this._deleteToTrash
|
|
721
|
+
return this._deleteToTrash
|
|
722
|
+
? 'moved to the trash when the system accepts them; a refused move may remove them permanently or leave them in place'
|
|
723
|
+
: 'deleted permanently';
|
|
685
724
|
}
|
|
686
725
|
async _removeProject(session) {
|
|
687
726
|
var _a;
|
|
@@ -816,7 +855,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
816
855
|
async _openSession(session, force, sessionId) {
|
|
817
856
|
// The click's own feedback, raised before the terminal walk and the
|
|
818
857
|
// launch - both can take seconds (DEF-PANE-318).
|
|
819
|
-
this._beginBusy(this._trans.__('Opening conversation'));
|
|
858
|
+
const busy = this._beginBusy(this._trans.__('Opening conversation'));
|
|
820
859
|
try {
|
|
821
860
|
await this._terminals.openSession(session, this._launchMode(force), sessionId);
|
|
822
861
|
// Not awaited: the open has succeeded, and the sync is not part of it.
|
|
@@ -831,7 +870,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
831
870
|
}
|
|
832
871
|
}
|
|
833
872
|
finally {
|
|
834
|
-
this._endBusy();
|
|
873
|
+
this._endBusy(busy);
|
|
835
874
|
// Reuse or fresh launch, either way the picture changed. Pull fresh
|
|
836
875
|
// state; a poll tick retries if this one fails.
|
|
837
876
|
void this._fetch().catch(() => undefined);
|
|
@@ -1063,12 +1102,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1063
1102
|
// Raised on the click: the branch POST mints the fork server-side (for
|
|
1064
1103
|
// Claude and Codex by running their own CLI) and can take seconds
|
|
1065
1104
|
// (DEF-PANE-318).
|
|
1066
|
-
this._beginBusy(this._trans.__('Branching conversation'));
|
|
1105
|
+
const busy = this._beginBusy(this._trans.__('Branching conversation'));
|
|
1067
1106
|
try {
|
|
1068
1107
|
await this._branchByServerCopy(session, undefined, force);
|
|
1069
1108
|
}
|
|
1070
1109
|
finally {
|
|
1071
|
-
this._endBusy();
|
|
1110
|
+
this._endBusy(busy);
|
|
1072
1111
|
}
|
|
1073
1112
|
}
|
|
1074
1113
|
/** The server mints the new id and answers with it before the terminal
|
package/lib/core/popup.js
CHANGED
|
@@ -44,9 +44,9 @@ export function showManageSessionsPopup(options) {
|
|
|
44
44
|
let items = [...options.branches];
|
|
45
45
|
const selected = new Set();
|
|
46
46
|
let deleting = false; // guards the async delete against double invocation
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
47
|
+
// Every deletion arms first and executes on the second click: the trash
|
|
48
|
+
// setting does not promise the system will accept the move, and a fallback
|
|
49
|
+
// removal is permanent.
|
|
50
50
|
let armed = false;
|
|
51
51
|
let armTimer = 0;
|
|
52
52
|
const body = document.createElement('div');
|
|
@@ -87,7 +87,7 @@ export function showManageSessionsPopup(options) {
|
|
|
87
87
|
deleteBtn.type = 'button';
|
|
88
88
|
deleteBtn.className = 'jp-AiAssistantsPanel-branchDelete';
|
|
89
89
|
deleteBtn.title = options.deleteToTrash
|
|
90
|
-
? 'Deleted conversations move to the trash
|
|
90
|
+
? 'Deleted conversations move to the trash when the system accepts them; a refused move may remove them permanently or leave them in place'
|
|
91
91
|
: 'Deleted conversations are removed permanently';
|
|
92
92
|
footer.appendChild(deleteBtn);
|
|
93
93
|
// Plain visual counter (selection, or the last action), beside the button
|
|
@@ -336,12 +336,12 @@ export function showManageSessionsPopup(options) {
|
|
|
336
336
|
return;
|
|
337
337
|
}
|
|
338
338
|
// No confirmation DIALOG here: a second Lumino dialog stacked on this popup
|
|
339
|
-
// renders detached.
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
if (
|
|
339
|
+
// renders detached. The deletion confirms in the footer itself - the first
|
|
340
|
+
// click arms the button, the second executes, and any selection change or
|
|
341
|
+
// four seconds of hesitation disarms it. Every deletion arms, because the
|
|
342
|
+
// trash setting does not promise the system will accept the move, and a
|
|
343
|
+
// refused move may remove the conversation permanently or leave it.
|
|
344
|
+
if (!armed) {
|
|
345
345
|
armed = true;
|
|
346
346
|
window.clearTimeout(armTimer);
|
|
347
347
|
armTimer = window.setTimeout(() => {
|
|
@@ -384,9 +384,9 @@ export function showManageSessionsPopup(options) {
|
|
|
384
384
|
(_a = options.onSync) === null || _a === void 0 ? void 0 : _a.call(options, items);
|
|
385
385
|
render();
|
|
386
386
|
updateControls();
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
387
|
+
// One wording for both outcomes: the client cannot know whether the
|
|
388
|
+
// trash accepted the move, so it does not claim it did.
|
|
389
|
+
const moved = `${deleted} deleted`;
|
|
390
390
|
selCount.textContent = moved;
|
|
391
391
|
selCount.classList.add('jp-mod-deleted');
|
|
392
392
|
// Clear then re-set on the next tick so an identical count (two single
|
package/lib/index.js
CHANGED
|
@@ -174,9 +174,9 @@ const plugin = {
|
|
|
174
174
|
descriptor: module.descriptor,
|
|
175
175
|
hooks: module.hooks,
|
|
176
176
|
rootDir: (_a = status === null || status === void 0 ? void 0 : status.root_dir) !== null && _a !== void 0 ? _a : '',
|
|
177
|
-
// Unknown reads as "not trash"
|
|
178
|
-
//
|
|
179
|
-
//
|
|
177
|
+
// Unknown reads as "not trash", so the dialogs and the popup
|
|
178
|
+
// present the permanent-delete wording rather than promising a
|
|
179
|
+
// recoverable move the server never confirmed (DEF-135).
|
|
180
180
|
deleteToTrash: status ? status.delete_to_trash !== false : false,
|
|
181
181
|
terminalTracker,
|
|
182
182
|
fileBrowser,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_ai_code_assistants_extension",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.115",
|
|
4
4
|
"description": "A full launcher and manager for every AI code assistant in JupyterLab - Claude Code, Codex, Kimi, Gemini and DeepSeek. Start, resume, fork, switch, and clean up CLI sessions from a side panel per assistant - one click lands you in the right terminal with the assistant already running, no duplicate tabs, no UUID hunting.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -213,13 +213,16 @@ describe('DEF-51 - the destructive dialogs open with Cancel under the keyboard',
|
|
|
213
213
|
expect(request).not.toHaveBeenCalled();
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
it('says a deletion
|
|
217
|
-
// Default: trash is on,
|
|
216
|
+
it('says a deletion may be permanent when the trash might refuse', async () => {
|
|
217
|
+
// Default: trash is on, but the server falls back to a permanent removal
|
|
218
|
+
// when no trash accepts the move.
|
|
218
219
|
let done = (panel as any)._removeProject(session()) as Promise<void>;
|
|
219
220
|
await openButtons();
|
|
220
221
|
let body = document.querySelector('.jp-Dialog-body')!.textContent ?? '';
|
|
221
|
-
expect(body).toContain(
|
|
222
|
-
|
|
222
|
+
expect(body).toContain(
|
|
223
|
+
'moved to the trash when the system accepts them; a refused move may remove them permanently or leave them in place'
|
|
224
|
+
);
|
|
225
|
+
expect(body).toContain('A permanent removal cannot be undone');
|
|
223
226
|
pressEnter();
|
|
224
227
|
await done;
|
|
225
228
|
|
|
@@ -562,6 +565,31 @@ describe('click-time busy state (DEF-PANE-318)', () => {
|
|
|
562
565
|
target.dispose();
|
|
563
566
|
});
|
|
564
567
|
|
|
568
|
+
it('names the action still running when an overlapping action ends first', () => {
|
|
569
|
+
const target = makePanel(DESCRIPTOR);
|
|
570
|
+
const label = (): HTMLElement =>
|
|
571
|
+
shellNodeOf(target).querySelector('.jp-AiAssistants-busyLabel')!;
|
|
572
|
+
const live = (): HTMLElement =>
|
|
573
|
+
target.node.querySelector('.jp-AiAssistantsPanel-srOnly')!;
|
|
574
|
+
|
|
575
|
+
const opening = (target as any)._beginBusy('Opening conversation');
|
|
576
|
+
const branching = (target as any)._beginBusy('Branching conversation');
|
|
577
|
+
expect(label().textContent).toBe('Branching conversation');
|
|
578
|
+
expect(live().textContent).toBe('Branching conversation');
|
|
579
|
+
|
|
580
|
+
// The later-started action ends first: the label and the live region must
|
|
581
|
+
// return to the one that is still running, not keep naming the finished
|
|
582
|
+
// action.
|
|
583
|
+
(target as any)._endBusy(branching);
|
|
584
|
+
expect(label().textContent).toBe('Opening conversation');
|
|
585
|
+
expect(live().textContent).toBe('Opening conversation');
|
|
586
|
+
|
|
587
|
+
(target as any)._endBusy(opening);
|
|
588
|
+
expect(label().textContent).toBe('');
|
|
589
|
+
expect(live().textContent).toBe('');
|
|
590
|
+
target.dispose();
|
|
591
|
+
});
|
|
592
|
+
|
|
565
593
|
it('announces the branch action while the request is pending', async () => {
|
|
566
594
|
const target = makePanel(DESCRIPTOR);
|
|
567
595
|
const parent = session({ name: 'analysis', name_source: 'session' });
|
|
@@ -158,9 +158,22 @@ describe('Manage Sessions popup Delete button (DEF-PANE-307)', () => {
|
|
|
158
158
|
expect(deleteButton().textContent).toBe('Delete (1)');
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
it('
|
|
161
|
+
it('names the trash outcome without promising recoverability', () => {
|
|
162
|
+
expect(deleteButton().title).toContain(
|
|
163
|
+
'a refused move may remove them permanently or leave them in place'
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('arms, then deletes without closing the dialog', async () => {
|
|
162
168
|
checkbox(rows()[1]).click();
|
|
163
|
-
deleteButton()
|
|
169
|
+
const del = deleteButton();
|
|
170
|
+
// The trash setting does not promise the system will accept the move, so
|
|
171
|
+
// the first click arms rather than deletes.
|
|
172
|
+
del.click();
|
|
173
|
+
expect(onDelete).not.toHaveBeenCalled();
|
|
174
|
+
expect(del.textContent).toBe('Confirm delete 1');
|
|
175
|
+
|
|
176
|
+
del.click();
|
|
164
177
|
await Promise.resolve();
|
|
165
178
|
expect(onDelete).toHaveBeenCalledWith([FIRST]);
|
|
166
179
|
expect(document.querySelector('.jp-Dialog')).not.toBeNull();
|
package/src/core/panel.ts
CHANGED
|
@@ -488,6 +488,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
488
488
|
'jp-aiAssistants-spinner jp-AiAssistants-busySpinner';
|
|
489
489
|
busySpinner.setAttribute('aria-hidden', 'true');
|
|
490
490
|
busy.appendChild(busySpinner);
|
|
491
|
+
// The action, named in sight: under a reduced-motion preference the ring
|
|
492
|
+
// is held still, so the label carries the busy state with no animation.
|
|
493
|
+
// Hidden from assistive technology because the live region below
|
|
494
|
+
// announces the same words once.
|
|
495
|
+
const busyLabel = document.createElement('div');
|
|
496
|
+
busyLabel.className = 'jp-AiAssistants-busyLabel';
|
|
497
|
+
busyLabel.setAttribute('aria-hidden', 'true');
|
|
498
|
+
busy.appendChild(busyLabel);
|
|
491
499
|
// The spinner is decoration; a screen-reader user hears the action
|
|
492
500
|
// instead. The region sits on the root, always rendered and outside the
|
|
493
501
|
// overlay and the aria-busy body: a live region created inside a
|
|
@@ -521,31 +529,42 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
521
529
|
this._bodyEl = body;
|
|
522
530
|
this._loadingEl = loading;
|
|
523
531
|
this._busyEl = busy;
|
|
532
|
+
this._busyLabelEl = busyLabel;
|
|
524
533
|
this._busyTextEl = busyText;
|
|
525
534
|
this._errorEl = error;
|
|
526
535
|
}
|
|
527
536
|
|
|
528
|
-
/** Raise the click-time busy overlay.
|
|
529
|
-
* can overlap; the overlay clears when the last one ends
|
|
530
|
-
|
|
531
|
-
|
|
537
|
+
/** Raise the click-time busy overlay. Each action gets a token, because an
|
|
538
|
+
* open and a branch can overlap; the overlay clears when the last one ends,
|
|
539
|
+
* and the label names the newest action still running. */
|
|
540
|
+
private _beginBusy(message: string): number {
|
|
541
|
+
const token = ++this._busySeq;
|
|
542
|
+
this._busyMessages.set(token, message);
|
|
532
543
|
if (this._busyEl) {
|
|
533
544
|
this._busyEl.hidden = false;
|
|
534
|
-
|
|
535
|
-
this._busyTextEl.textContent = message;
|
|
536
|
-
}
|
|
545
|
+
this._paintBusyMessage();
|
|
537
546
|
// The body is what the action changes; the root stays outside the
|
|
538
547
|
// busy subtree so the live region above is not withheld.
|
|
539
548
|
if (this._bodyEl) {
|
|
540
549
|
this._bodyEl.setAttribute('aria-busy', 'true');
|
|
541
550
|
}
|
|
542
551
|
}
|
|
552
|
+
return token;
|
|
543
553
|
}
|
|
544
554
|
|
|
545
|
-
private _endBusy(): void {
|
|
546
|
-
this.
|
|
547
|
-
if (this.
|
|
555
|
+
private _endBusy(token: number): void {
|
|
556
|
+
this._busyMessages.delete(token);
|
|
557
|
+
if (this._busyMessages.size > 0) {
|
|
558
|
+
// Another action is still running; neither the visible label nor the
|
|
559
|
+
// live region may keep naming the one that just ended.
|
|
560
|
+
this._paintBusyMessage();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (this._busyEl) {
|
|
548
564
|
this._busyEl.hidden = true;
|
|
565
|
+
if (this._busyLabelEl) {
|
|
566
|
+
this._busyLabelEl.textContent = '';
|
|
567
|
+
}
|
|
549
568
|
if (this._busyTextEl) {
|
|
550
569
|
this._busyTextEl.textContent = '';
|
|
551
570
|
}
|
|
@@ -555,6 +574,21 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
555
574
|
}
|
|
556
575
|
}
|
|
557
576
|
|
|
577
|
+
/** Name the newest still-running action in the visible label and the live
|
|
578
|
+
* region, so neither keeps naming an action that ended. */
|
|
579
|
+
private _paintBusyMessage(): void {
|
|
580
|
+
let message = '';
|
|
581
|
+
for (const active of this._busyMessages.values()) {
|
|
582
|
+
message = active;
|
|
583
|
+
}
|
|
584
|
+
if (this._busyLabelEl) {
|
|
585
|
+
this._busyLabelEl.textContent = message;
|
|
586
|
+
}
|
|
587
|
+
if (this._busyTextEl) {
|
|
588
|
+
this._busyTextEl.textContent = message;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
558
592
|
/** Show / hide the filter input. Hiding also clears the active filter, so
|
|
559
593
|
* the user never returns to an "invisible" filter narrowing the rows. */
|
|
560
594
|
private _toggleFilterBar(): void {
|
|
@@ -781,15 +815,19 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
781
815
|
}
|
|
782
816
|
}
|
|
783
817
|
|
|
784
|
-
/** The warning a
|
|
785
|
-
*
|
|
786
|
-
*
|
|
818
|
+
/** The warning a deletion earns: with the trash setting on, a refused move
|
|
819
|
+
* may remove the conversations permanently or leave them in place, so
|
|
820
|
+
* neither the verb nor the note may promise one outcome. */
|
|
787
821
|
private get _irreversibleNote(): string {
|
|
788
|
-
return this._deleteToTrash
|
|
822
|
+
return this._deleteToTrash
|
|
823
|
+
? ' A permanent removal cannot be undone.'
|
|
824
|
+
: ' This cannot be undone.';
|
|
789
825
|
}
|
|
790
826
|
|
|
791
827
|
private get _disposalVerb(): string {
|
|
792
|
-
return this._deleteToTrash
|
|
828
|
+
return this._deleteToTrash
|
|
829
|
+
? 'moved to the trash when the system accepts them; a refused move may remove them permanently or leave them in place'
|
|
830
|
+
: 'deleted permanently';
|
|
793
831
|
}
|
|
794
832
|
|
|
795
833
|
private async _removeProject(session: ISession): Promise<void> {
|
|
@@ -954,7 +992,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
954
992
|
): Promise<void> {
|
|
955
993
|
// The click's own feedback, raised before the terminal walk and the
|
|
956
994
|
// launch - both can take seconds (DEF-PANE-318).
|
|
957
|
-
this._beginBusy(this._trans.__('Opening conversation'));
|
|
995
|
+
const busy = this._beginBusy(this._trans.__('Opening conversation'));
|
|
958
996
|
try {
|
|
959
997
|
await this._terminals.openSession(
|
|
960
998
|
session,
|
|
@@ -975,7 +1013,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
975
1013
|
this._notifyLaunchError('Could not open that conversation', err);
|
|
976
1014
|
}
|
|
977
1015
|
} finally {
|
|
978
|
-
this._endBusy();
|
|
1016
|
+
this._endBusy(busy);
|
|
979
1017
|
// Reuse or fresh launch, either way the picture changed. Pull fresh
|
|
980
1018
|
// state; a poll tick retries if this one fails.
|
|
981
1019
|
void this._fetch().catch(() => undefined);
|
|
@@ -1239,11 +1277,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1239
1277
|
// Raised on the click: the branch POST mints the fork server-side (for
|
|
1240
1278
|
// Claude and Codex by running their own CLI) and can take seconds
|
|
1241
1279
|
// (DEF-PANE-318).
|
|
1242
|
-
this._beginBusy(this._trans.__('Branching conversation'));
|
|
1280
|
+
const busy = this._beginBusy(this._trans.__('Branching conversation'));
|
|
1243
1281
|
try {
|
|
1244
1282
|
await this._branchByServerCopy(session, undefined, force);
|
|
1245
1283
|
} finally {
|
|
1246
|
-
this._endBusy();
|
|
1284
|
+
this._endBusy(busy);
|
|
1247
1285
|
}
|
|
1248
1286
|
}
|
|
1249
1287
|
|
|
@@ -2858,8 +2896,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2858
2896
|
private _bodyEl!: HTMLDivElement;
|
|
2859
2897
|
private _loadingEl: HTMLElement | null = null;
|
|
2860
2898
|
private _busyEl: HTMLElement | null = null;
|
|
2899
|
+
private _busyLabelEl: HTMLElement | null = null;
|
|
2861
2900
|
private _busyTextEl: HTMLElement | null = null;
|
|
2862
|
-
private
|
|
2901
|
+
private _busySeq = 0;
|
|
2902
|
+
private _busyMessages = new Map<number, string>();
|
|
2863
2903
|
private _errorEl: HTMLElement | null = null;
|
|
2864
2904
|
private _refreshBtn: HTMLButtonElement | null = null;
|
|
2865
2905
|
private _filterBtn: HTMLButtonElement | null = null;
|
package/src/core/popup.ts
CHANGED
|
@@ -106,9 +106,9 @@ export function showManageSessionsPopup(
|
|
|
106
106
|
let items = [...options.branches];
|
|
107
107
|
const selected = new Set<string>();
|
|
108
108
|
let deleting = false; // guards the async delete against double invocation
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
109
|
+
// Every deletion arms first and executes on the second click: the trash
|
|
110
|
+
// setting does not promise the system will accept the move, and a fallback
|
|
111
|
+
// removal is permanent.
|
|
112
112
|
let armed = false;
|
|
113
113
|
let armTimer = 0;
|
|
114
114
|
|
|
@@ -154,7 +154,7 @@ export function showManageSessionsPopup(
|
|
|
154
154
|
deleteBtn.type = 'button';
|
|
155
155
|
deleteBtn.className = 'jp-AiAssistantsPanel-branchDelete';
|
|
156
156
|
deleteBtn.title = options.deleteToTrash
|
|
157
|
-
? 'Deleted conversations move to the trash
|
|
157
|
+
? 'Deleted conversations move to the trash when the system accepts them; a refused move may remove them permanently or leave them in place'
|
|
158
158
|
: 'Deleted conversations are removed permanently';
|
|
159
159
|
footer.appendChild(deleteBtn);
|
|
160
160
|
// Plain visual counter (selection, or the last action), beside the button
|
|
@@ -421,12 +421,12 @@ export function showManageSessionsPopup(
|
|
|
421
421
|
return;
|
|
422
422
|
}
|
|
423
423
|
// No confirmation DIALOG here: a second Lumino dialog stacked on this popup
|
|
424
|
-
// renders detached.
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
if (
|
|
424
|
+
// renders detached. The deletion confirms in the footer itself - the first
|
|
425
|
+
// click arms the button, the second executes, and any selection change or
|
|
426
|
+
// four seconds of hesitation disarms it. Every deletion arms, because the
|
|
427
|
+
// trash setting does not promise the system will accept the move, and a
|
|
428
|
+
// refused move may remove the conversation permanently or leave it.
|
|
429
|
+
if (!armed) {
|
|
430
430
|
armed = true;
|
|
431
431
|
window.clearTimeout(armTimer);
|
|
432
432
|
armTimer = window.setTimeout(() => {
|
|
@@ -467,9 +467,9 @@ export function showManageSessionsPopup(
|
|
|
467
467
|
options.onSync?.(items);
|
|
468
468
|
render();
|
|
469
469
|
updateControls();
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
470
|
+
// One wording for both outcomes: the client cannot know whether the
|
|
471
|
+
// trash accepted the move, so it does not claim it did.
|
|
472
|
+
const moved = `${deleted} deleted`;
|
|
473
473
|
selCount.textContent = moved;
|
|
474
474
|
selCount.classList.add('jp-mod-deleted');
|
|
475
475
|
// Clear then re-set on the next tick so an identical count (two single
|
package/src/index.ts
CHANGED
|
@@ -250,9 +250,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
250
250
|
descriptor: module.descriptor,
|
|
251
251
|
hooks: module.hooks,
|
|
252
252
|
rootDir: status?.root_dir ?? '',
|
|
253
|
-
// Unknown reads as "not trash"
|
|
254
|
-
//
|
|
255
|
-
//
|
|
253
|
+
// Unknown reads as "not trash", so the dialogs and the popup
|
|
254
|
+
// present the permanent-delete wording rather than promising a
|
|
255
|
+
// recoverable move the server never confirmed (DEF-135).
|
|
256
256
|
deleteToTrash: status ? status.delete_to_trash !== false : false,
|
|
257
257
|
terminalTracker,
|
|
258
258
|
fileBrowser,
|
package/style/base.css
CHANGED
|
@@ -562,7 +562,9 @@ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotHalo {
|
|
|
562
562
|
|
|
563
563
|
/* The click-time busy spinner is the only thing on screen while a click is
|
|
564
564
|
being handled, so it keeps full opacity: dimmed, its brand arc measures
|
|
565
|
-
1.74:1 on the light panel, below the 3:1 minimum for a non-text cue.
|
|
565
|
+
1.74:1 on the light panel, below the 3:1 minimum for a non-text cue. Held
|
|
566
|
+
still it would read as idle, so the overlay names the action in a visible
|
|
567
|
+
label beside it; the label, not an animation, carries the busy state. */
|
|
566
568
|
.jp-AiAssistants-busySpinner {
|
|
567
569
|
opacity: 1;
|
|
568
570
|
}
|
|
@@ -601,6 +603,7 @@ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotHalo {
|
|
|
601
603
|
display: flex;
|
|
602
604
|
align-items: center;
|
|
603
605
|
justify-content: center;
|
|
606
|
+
gap: 12px;
|
|
604
607
|
pointer-events: none;
|
|
605
608
|
}
|
|
606
609
|
|
|
@@ -610,6 +613,15 @@ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotHalo {
|
|
|
610
613
|
border-width: 3px;
|
|
611
614
|
}
|
|
612
615
|
|
|
616
|
+
.jp-AiAssistants-busyLabel {
|
|
617
|
+
color: var(--jp-ui-font-color1);
|
|
618
|
+
background: var(--jp-layout-color1);
|
|
619
|
+
border: 1px solid var(--jp-border-color2);
|
|
620
|
+
border-radius: 4px;
|
|
621
|
+
padding: 4px 10px;
|
|
622
|
+
font-size: var(--jp-ui-font-size1);
|
|
623
|
+
}
|
|
624
|
+
|
|
613
625
|
.jp-AiAssistantsPanel-launchOverlay {
|
|
614
626
|
display: flex;
|
|
615
627
|
flex-direction: column;
|