jupyterlab_ai_code_assistants_extension 1.2.83 → 1.2.99
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/lib/core/panel.d.ts +7 -0
- package/lib/core/panel.js +71 -1
- package/package.json +1 -1
- package/src/__tests__/panel.spec.ts +157 -0
- package/src/core/panel.ts +73 -1
- package/style/base.css +28 -1
package/lib/core/panel.d.ts
CHANGED
|
@@ -45,6 +45,10 @@ 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. Counted, because an open and a branch
|
|
49
|
+
* can overlap; the overlay clears when the last one ends. */
|
|
50
|
+
private _beginBusy;
|
|
51
|
+
private _endBusy;
|
|
48
52
|
/** Show / hide the filter input. Hiding also clears the active filter, so
|
|
49
53
|
* the user never returns to an "invisible" filter narrowing the rows. */
|
|
50
54
|
private _toggleFilterBar;
|
|
@@ -302,6 +306,9 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
302
306
|
private readonly _removingPaths;
|
|
303
307
|
private _bodyEl;
|
|
304
308
|
private _loadingEl;
|
|
309
|
+
private _busyEl;
|
|
310
|
+
private _busyTextEl;
|
|
311
|
+
private _busyCount;
|
|
305
312
|
private _errorEl;
|
|
306
313
|
private _refreshBtn;
|
|
307
314
|
private _filterBtn;
|
package/lib/core/panel.js
CHANGED
|
@@ -112,6 +112,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
112
112
|
this._deleteToTrash = true;
|
|
113
113
|
this._removingPaths = new Set();
|
|
114
114
|
this._loadingEl = null;
|
|
115
|
+
this._busyEl = null;
|
|
116
|
+
this._busyTextEl = null;
|
|
117
|
+
this._busyCount = 0;
|
|
115
118
|
this._errorEl = null;
|
|
116
119
|
this._refreshBtn = null;
|
|
117
120
|
this._filterBtn = null;
|
|
@@ -389,6 +392,28 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
389
392
|
loadingSpinner.className =
|
|
390
393
|
'jp-aiAssistants-spinner jp-AiAssistantsPanel-loadingSpinner';
|
|
391
394
|
loading.appendChild(loadingSpinner);
|
|
395
|
+
// Click-time busy overlay: raised synchronously when a row is opened or
|
|
396
|
+
// branched, before the terminal probe or the branch request, so the click
|
|
397
|
+
// never reads as ignored (DEF-PANE-318). Unlike the refresh veil it takes
|
|
398
|
+
// no pointer events and no focus: the context menu that launched a branch
|
|
399
|
+
// stays open until its command runs, and a focus change would close it.
|
|
400
|
+
const busy = document.createElement('div');
|
|
401
|
+
busy.className = 'jp-AiAssistantsPanel-busyOverlay';
|
|
402
|
+
busy.hidden = true;
|
|
403
|
+
const busySpinner = document.createElement('div');
|
|
404
|
+
busySpinner.className =
|
|
405
|
+
'jp-aiAssistants-spinner jp-AiAssistantsPanel-busySpinner';
|
|
406
|
+
busySpinner.setAttribute('aria-hidden', 'true');
|
|
407
|
+
busy.appendChild(busySpinner);
|
|
408
|
+
// The spinner is decoration; a screen-reader user hears the action
|
|
409
|
+
// instead. The region sits on the root, always rendered and outside the
|
|
410
|
+
// overlay and the aria-busy body: a live region created inside a
|
|
411
|
+
// display:none container, or one whose update lands in an aria-busy
|
|
412
|
+
// subtree, can be withheld by assistive technology.
|
|
413
|
+
const busyText = document.createElement('div');
|
|
414
|
+
busyText.className = 'jp-AiAssistantsPanel-srOnly';
|
|
415
|
+
busyText.setAttribute('role', 'status');
|
|
416
|
+
busyText.setAttribute('aria-live', 'polite');
|
|
392
417
|
// Inline error strip - a failed poll reports here and keeps retrying,
|
|
393
418
|
// rather than tearing the panel down or raising a modal that would also
|
|
394
419
|
// interrupt the other providers' panels.
|
|
@@ -403,10 +428,42 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
403
428
|
root.appendChild(error);
|
|
404
429
|
root.appendChild(body);
|
|
405
430
|
root.appendChild(loading);
|
|
431
|
+
root.appendChild(busy);
|
|
432
|
+
root.appendChild(busyText);
|
|
406
433
|
this._bodyEl = body;
|
|
407
434
|
this._loadingEl = loading;
|
|
435
|
+
this._busyEl = busy;
|
|
436
|
+
this._busyTextEl = busyText;
|
|
408
437
|
this._errorEl = error;
|
|
409
438
|
}
|
|
439
|
+
/** Raise the click-time busy overlay. Counted, because an open and a branch
|
|
440
|
+
* can overlap; the overlay clears when the last one ends. */
|
|
441
|
+
_beginBusy(message) {
|
|
442
|
+
this._busyCount += 1;
|
|
443
|
+
if (this._busyEl) {
|
|
444
|
+
this._busyEl.hidden = false;
|
|
445
|
+
if (this._busyTextEl) {
|
|
446
|
+
this._busyTextEl.textContent = message;
|
|
447
|
+
}
|
|
448
|
+
// The body is what the action changes; the root stays outside the
|
|
449
|
+
// busy subtree so the live region above is not withheld.
|
|
450
|
+
if (this._bodyEl) {
|
|
451
|
+
this._bodyEl.setAttribute('aria-busy', 'true');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
_endBusy() {
|
|
456
|
+
this._busyCount = Math.max(0, this._busyCount - 1);
|
|
457
|
+
if (this._busyCount === 0 && this._busyEl) {
|
|
458
|
+
this._busyEl.hidden = true;
|
|
459
|
+
if (this._busyTextEl) {
|
|
460
|
+
this._busyTextEl.textContent = '';
|
|
461
|
+
}
|
|
462
|
+
if (this._bodyEl) {
|
|
463
|
+
this._bodyEl.removeAttribute('aria-busy');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
410
467
|
/** Show / hide the filter input. Hiding also clears the active filter, so
|
|
411
468
|
* the user never returns to an "invisible" filter narrowing the rows. */
|
|
412
469
|
_toggleFilterBar() {
|
|
@@ -746,6 +803,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
746
803
|
return resolveLaunchMode(this._descriptor.launchModes, this._modes, force);
|
|
747
804
|
}
|
|
748
805
|
async _openSession(session, force, sessionId) {
|
|
806
|
+
// The click's own feedback, raised before the terminal walk and the
|
|
807
|
+
// launch - both can take seconds (DEF-PANE-318).
|
|
808
|
+
this._beginBusy(this._trans.__('Opening conversation'));
|
|
749
809
|
try {
|
|
750
810
|
await this._terminals.openSession(session, this._launchMode(force), sessionId);
|
|
751
811
|
// Not awaited: the open has succeeded, and the sync is not part of it.
|
|
@@ -760,6 +820,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
760
820
|
}
|
|
761
821
|
}
|
|
762
822
|
finally {
|
|
823
|
+
this._endBusy();
|
|
763
824
|
// Reuse or fresh launch, either way the picture changed. Pull fresh
|
|
764
825
|
// state; a poll tick retries if this one fails.
|
|
765
826
|
void this._fetch().catch(() => undefined);
|
|
@@ -985,7 +1046,16 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
985
1046
|
if (!session) {
|
|
986
1047
|
return;
|
|
987
1048
|
}
|
|
988
|
-
|
|
1049
|
+
// Raised on the click: the branch POST mints the fork server-side (for
|
|
1050
|
+
// Claude and Codex by running their own CLI) and can take seconds
|
|
1051
|
+
// (DEF-PANE-318).
|
|
1052
|
+
this._beginBusy(this._trans.__('Branching conversation'));
|
|
1053
|
+
try {
|
|
1054
|
+
await this._branchByServerCopy(session, undefined, force);
|
|
1055
|
+
}
|
|
1056
|
+
finally {
|
|
1057
|
+
this._endBusy();
|
|
1058
|
+
}
|
|
989
1059
|
}
|
|
990
1060
|
/** The server mints the new id and answers with it before the terminal
|
|
991
1061
|
* opens - `server-copy` copies the store entry, `server-native` drives the
|
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.99",
|
|
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",
|
|
@@ -455,6 +455,146 @@ describe('ACC-SESS-203 - automatic branch names', () => {
|
|
|
455
455
|
);
|
|
456
456
|
});
|
|
457
457
|
|
|
458
|
+
describe('click-time busy state (DEF-PANE-318)', () => {
|
|
459
|
+
it('raises the overlay and the body busy state on the click, before the open settles', async () => {
|
|
460
|
+
const target = makePanel(DESCRIPTOR);
|
|
461
|
+
const parent = session();
|
|
462
|
+
let release!: () => void;
|
|
463
|
+
const pending = new Promise<void>(resolve => {
|
|
464
|
+
release = resolve;
|
|
465
|
+
});
|
|
466
|
+
(target as any)._terminals.openSession = jest.fn(() => pending);
|
|
467
|
+
(target as any)._fetch = jest.fn().mockResolvedValue(undefined);
|
|
468
|
+
const overlay = (): HTMLElement =>
|
|
469
|
+
target.node.querySelector('.jp-AiAssistantsPanel-busyOverlay')!;
|
|
470
|
+
const body = (): HTMLElement =>
|
|
471
|
+
target.node.querySelector('.jp-AiAssistantsPanel-body')!;
|
|
472
|
+
|
|
473
|
+
const live = target.node.querySelector(
|
|
474
|
+
'.jp-AiAssistantsPanel-srOnly'
|
|
475
|
+
) as HTMLElement;
|
|
476
|
+
// Rendered at build, not created on demand: the region must be in the
|
|
477
|
+
// accessibility tree before the click, so its first update is announced.
|
|
478
|
+
expect(live).not.toBeNull();
|
|
479
|
+
expect(live.textContent).toBe('');
|
|
480
|
+
expect(live.getAttribute('role')).toBe('status');
|
|
481
|
+
expect(live.getAttribute('aria-live')).toBe('polite');
|
|
482
|
+
|
|
483
|
+
expect(overlay().hidden).toBe(true);
|
|
484
|
+
// Root placement is load-bearing: _render wipes the body on every fetch,
|
|
485
|
+
// including the 30 s poll, which would delete an overlay kept there.
|
|
486
|
+
expect(overlay().parentElement).toBe(target.node);
|
|
487
|
+
// The overlay takes no focus, so the context menu that launched an action
|
|
488
|
+
// cannot be closed by an element inside it taking the focus.
|
|
489
|
+
expect(overlay().hasAttribute('tabindex')).toBe(false);
|
|
490
|
+
expect(
|
|
491
|
+
overlay().querySelectorAll(
|
|
492
|
+
'a[href], button, input, select, textarea, [tabindex]'
|
|
493
|
+
)
|
|
494
|
+
).toHaveLength(0);
|
|
495
|
+
|
|
496
|
+
const call = (target as any)._openSession(parent);
|
|
497
|
+
// The click's own task, before the open settles.
|
|
498
|
+
expect(overlay().hidden).toBe(false);
|
|
499
|
+
expect(body().getAttribute('aria-busy')).toBe('true');
|
|
500
|
+
// Outside both the hidden overlay and the busy body, so assistive
|
|
501
|
+
// technology is not told to withhold the announcement.
|
|
502
|
+
expect(live.parentElement).toBe(target.node);
|
|
503
|
+
expect(body().contains(live)).toBe(false);
|
|
504
|
+
expect(overlay().contains(live)).toBe(false);
|
|
505
|
+
expect(live.textContent).toBe('Opening conversation');
|
|
506
|
+
|
|
507
|
+
release();
|
|
508
|
+
await call;
|
|
509
|
+
expect(overlay().hidden).toBe(true);
|
|
510
|
+
expect(body().getAttribute('aria-busy')).toBeNull();
|
|
511
|
+
// Cleared, so an identical action can announce again.
|
|
512
|
+
expect(live.textContent).toBe('');
|
|
513
|
+
|
|
514
|
+
target.dispose();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('keeps the overlay up while a second action is still running', async () => {
|
|
518
|
+
const target = makePanel(DESCRIPTOR);
|
|
519
|
+
let releaseFirst!: () => void;
|
|
520
|
+
let releaseSecond!: () => void;
|
|
521
|
+
const first = new Promise<void>(resolve => {
|
|
522
|
+
releaseFirst = resolve;
|
|
523
|
+
});
|
|
524
|
+
const second = new Promise<void>(resolve => {
|
|
525
|
+
releaseSecond = resolve;
|
|
526
|
+
});
|
|
527
|
+
const requests = [first, second];
|
|
528
|
+
(target as any)._terminals.openSession = jest.fn(() => requests.shift());
|
|
529
|
+
(target as any)._fetch = jest.fn().mockResolvedValue(undefined);
|
|
530
|
+
const overlay = (): HTMLElement =>
|
|
531
|
+
target.node.querySelector('.jp-AiAssistantsPanel-busyOverlay')!;
|
|
532
|
+
const body = (): HTMLElement =>
|
|
533
|
+
target.node.querySelector('.jp-AiAssistantsPanel-body')!;
|
|
534
|
+
|
|
535
|
+
const callOne = (target as any)._openSession(
|
|
536
|
+
session({ session_id: 'one' })
|
|
537
|
+
);
|
|
538
|
+
const callTwo = (target as any)._openSession(
|
|
539
|
+
session({ session_id: 'two' })
|
|
540
|
+
);
|
|
541
|
+
expect(overlay().hidden).toBe(false);
|
|
542
|
+
|
|
543
|
+
releaseFirst();
|
|
544
|
+
await callOne;
|
|
545
|
+
expect(overlay().hidden).toBe(false);
|
|
546
|
+
expect(body().getAttribute('aria-busy')).toBe('true');
|
|
547
|
+
|
|
548
|
+
releaseSecond();
|
|
549
|
+
await callTwo;
|
|
550
|
+
expect(overlay().hidden).toBe(true);
|
|
551
|
+
expect(body().getAttribute('aria-busy')).toBeNull();
|
|
552
|
+
|
|
553
|
+
target.dispose();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('announces the branch action while the request is pending', async () => {
|
|
557
|
+
const target = makePanel(DESCRIPTOR);
|
|
558
|
+
const parent = session({ name: 'analysis', name_source: 'session' });
|
|
559
|
+
const launch = jest.fn().mockResolvedValue({});
|
|
560
|
+
(target as any)._terminals.launch = launch;
|
|
561
|
+
(target as any)._activeSession = parent;
|
|
562
|
+
(target as any)._fetch = jest.fn().mockResolvedValue(undefined);
|
|
563
|
+
let release!: (value: unknown) => void;
|
|
564
|
+
const pending = new Promise(resolve => {
|
|
565
|
+
release = resolve;
|
|
566
|
+
});
|
|
567
|
+
request.mockImplementation((_provider, path) =>
|
|
568
|
+
path === 'branch' ? pending : Promise.resolve({})
|
|
569
|
+
);
|
|
570
|
+
const body = (): HTMLElement =>
|
|
571
|
+
target.node.querySelector('.jp-AiAssistantsPanel-body')!;
|
|
572
|
+
const live = target.node.querySelector(
|
|
573
|
+
'.jp-AiAssistantsPanel-srOnly'
|
|
574
|
+
) as HTMLElement;
|
|
575
|
+
let call: Promise<void> | undefined;
|
|
576
|
+
try {
|
|
577
|
+
call = (target as any)._branchSession();
|
|
578
|
+
expect(body().getAttribute('aria-busy')).toBe('true');
|
|
579
|
+
expect(live.textContent).toBe('Branching conversation');
|
|
580
|
+
|
|
581
|
+
release({ session_id: 'child' });
|
|
582
|
+
await call;
|
|
583
|
+
expect(body().getAttribute('aria-busy')).toBeNull();
|
|
584
|
+
expect(live.textContent).toBe('');
|
|
585
|
+
} finally {
|
|
586
|
+
// A failed assertion must not leave the mock unresolved for the next
|
|
587
|
+
// test; resolving twice is harmless.
|
|
588
|
+
release({ session_id: 'child' });
|
|
589
|
+
if (call) {
|
|
590
|
+
await call.catch(() => undefined);
|
|
591
|
+
}
|
|
592
|
+
target.dispose();
|
|
593
|
+
request.mockImplementation(() => Promise.resolve({}));
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
458
598
|
describe('a branch whose terminal launch was refused after the mint', () => {
|
|
459
599
|
it('says the branch was created and still lists it when the launch is refused', async () => {
|
|
460
600
|
// The DeepSeek preflight refuses the follow-up launch with a 409 whose
|
|
@@ -493,6 +633,15 @@ describe('a branch whose terminal launch was refused after the mint', () => {
|
|
|
493
633
|
try {
|
|
494
634
|
await (target as any)._branchSession();
|
|
495
635
|
|
|
636
|
+
// The refused launch still clears the click-time busy state.
|
|
637
|
+
expect(
|
|
638
|
+
(
|
|
639
|
+
target.node.querySelector(
|
|
640
|
+
'.jp-AiAssistantsPanel-busyOverlay'
|
|
641
|
+
) as HTMLElement
|
|
642
|
+
).hidden
|
|
643
|
+
).toBe(true);
|
|
644
|
+
|
|
496
645
|
// What the user is told: the branch was created, its terminal did not
|
|
497
646
|
// open, and the refusal's own words - neither sentence that blames the
|
|
498
647
|
// branch itself.
|
|
@@ -1820,6 +1969,14 @@ describe('ACC-SESS-190..192 - opening a session moves the file browser', () => {
|
|
|
1820
1969
|
|
|
1821
1970
|
expect(opened).toHaveBeenCalledTimes(1);
|
|
1822
1971
|
expect(moves(panel)).toEqual([]);
|
|
1972
|
+
// The failed open clears the click-time busy state.
|
|
1973
|
+
expect(
|
|
1974
|
+
(
|
|
1975
|
+
panel.node.querySelector(
|
|
1976
|
+
'.jp-AiAssistantsPanel-busyOverlay'
|
|
1977
|
+
) as HTMLElement
|
|
1978
|
+
).hidden
|
|
1979
|
+
).toBe(true);
|
|
1823
1980
|
// One notification, and it is the one the failed open raises itself.
|
|
1824
1981
|
expect(notified()).toHaveLength(1);
|
|
1825
1982
|
expect(notified()[0]).toContain('Could not open that conversation');
|
package/src/core/panel.ts
CHANGED
|
@@ -467,6 +467,29 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
467
467
|
'jp-aiAssistants-spinner jp-AiAssistantsPanel-loadingSpinner';
|
|
468
468
|
loading.appendChild(loadingSpinner);
|
|
469
469
|
|
|
470
|
+
// Click-time busy overlay: raised synchronously when a row is opened or
|
|
471
|
+
// branched, before the terminal probe or the branch request, so the click
|
|
472
|
+
// never reads as ignored (DEF-PANE-318). Unlike the refresh veil it takes
|
|
473
|
+
// no pointer events and no focus: the context menu that launched a branch
|
|
474
|
+
// stays open until its command runs, and a focus change would close it.
|
|
475
|
+
const busy = document.createElement('div');
|
|
476
|
+
busy.className = 'jp-AiAssistantsPanel-busyOverlay';
|
|
477
|
+
busy.hidden = true;
|
|
478
|
+
const busySpinner = document.createElement('div');
|
|
479
|
+
busySpinner.className =
|
|
480
|
+
'jp-aiAssistants-spinner jp-AiAssistantsPanel-busySpinner';
|
|
481
|
+
busySpinner.setAttribute('aria-hidden', 'true');
|
|
482
|
+
busy.appendChild(busySpinner);
|
|
483
|
+
// The spinner is decoration; a screen-reader user hears the action
|
|
484
|
+
// instead. The region sits on the root, always rendered and outside the
|
|
485
|
+
// overlay and the aria-busy body: a live region created inside a
|
|
486
|
+
// display:none container, or one whose update lands in an aria-busy
|
|
487
|
+
// subtree, can be withheld by assistive technology.
|
|
488
|
+
const busyText = document.createElement('div');
|
|
489
|
+
busyText.className = 'jp-AiAssistantsPanel-srOnly';
|
|
490
|
+
busyText.setAttribute('role', 'status');
|
|
491
|
+
busyText.setAttribute('aria-live', 'polite');
|
|
492
|
+
|
|
470
493
|
// Inline error strip - a failed poll reports here and keeps retrying,
|
|
471
494
|
// rather than tearing the panel down or raising a modal that would also
|
|
472
495
|
// interrupt the other providers' panels.
|
|
@@ -482,12 +505,46 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
482
505
|
root.appendChild(error);
|
|
483
506
|
root.appendChild(body);
|
|
484
507
|
root.appendChild(loading);
|
|
508
|
+
root.appendChild(busy);
|
|
509
|
+
root.appendChild(busyText);
|
|
485
510
|
|
|
486
511
|
this._bodyEl = body;
|
|
487
512
|
this._loadingEl = loading;
|
|
513
|
+
this._busyEl = busy;
|
|
514
|
+
this._busyTextEl = busyText;
|
|
488
515
|
this._errorEl = error;
|
|
489
516
|
}
|
|
490
517
|
|
|
518
|
+
/** Raise the click-time busy overlay. Counted, because an open and a branch
|
|
519
|
+
* can overlap; the overlay clears when the last one ends. */
|
|
520
|
+
private _beginBusy(message: string): void {
|
|
521
|
+
this._busyCount += 1;
|
|
522
|
+
if (this._busyEl) {
|
|
523
|
+
this._busyEl.hidden = false;
|
|
524
|
+
if (this._busyTextEl) {
|
|
525
|
+
this._busyTextEl.textContent = message;
|
|
526
|
+
}
|
|
527
|
+
// The body is what the action changes; the root stays outside the
|
|
528
|
+
// busy subtree so the live region above is not withheld.
|
|
529
|
+
if (this._bodyEl) {
|
|
530
|
+
this._bodyEl.setAttribute('aria-busy', 'true');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private _endBusy(): void {
|
|
536
|
+
this._busyCount = Math.max(0, this._busyCount - 1);
|
|
537
|
+
if (this._busyCount === 0 && this._busyEl) {
|
|
538
|
+
this._busyEl.hidden = true;
|
|
539
|
+
if (this._busyTextEl) {
|
|
540
|
+
this._busyTextEl.textContent = '';
|
|
541
|
+
}
|
|
542
|
+
if (this._bodyEl) {
|
|
543
|
+
this._bodyEl.removeAttribute('aria-busy');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
491
548
|
/** Show / hide the filter input. Hiding also clears the active filter, so
|
|
492
549
|
* the user never returns to an "invisible" filter narrowing the rows. */
|
|
493
550
|
private _toggleFilterBar(): void {
|
|
@@ -885,6 +942,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
885
942
|
force?: string,
|
|
886
943
|
sessionId?: string
|
|
887
944
|
): Promise<void> {
|
|
945
|
+
// The click's own feedback, raised before the terminal walk and the
|
|
946
|
+
// launch - both can take seconds (DEF-PANE-318).
|
|
947
|
+
this._beginBusy(this._trans.__('Opening conversation'));
|
|
888
948
|
try {
|
|
889
949
|
await this._terminals.openSession(
|
|
890
950
|
session,
|
|
@@ -905,6 +965,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
905
965
|
this._notifyLaunchError('Could not open that conversation', err);
|
|
906
966
|
}
|
|
907
967
|
} finally {
|
|
968
|
+
this._endBusy();
|
|
908
969
|
// Reuse or fresh launch, either way the picture changed. Pull fresh
|
|
909
970
|
// state; a poll tick retries if this one fails.
|
|
910
971
|
void this._fetch().catch(() => undefined);
|
|
@@ -1159,7 +1220,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1159
1220
|
if (!session) {
|
|
1160
1221
|
return;
|
|
1161
1222
|
}
|
|
1162
|
-
|
|
1223
|
+
// Raised on the click: the branch POST mints the fork server-side (for
|
|
1224
|
+
// Claude and Codex by running their own CLI) and can take seconds
|
|
1225
|
+
// (DEF-PANE-318).
|
|
1226
|
+
this._beginBusy(this._trans.__('Branching conversation'));
|
|
1227
|
+
try {
|
|
1228
|
+
await this._branchByServerCopy(session, undefined, force);
|
|
1229
|
+
} finally {
|
|
1230
|
+
this._endBusy();
|
|
1231
|
+
}
|
|
1163
1232
|
}
|
|
1164
1233
|
|
|
1165
1234
|
/** The server mints the new id and answers with it before the terminal
|
|
@@ -2785,6 +2854,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2785
2854
|
private readonly _removingPaths: Set<string> = new Set();
|
|
2786
2855
|
private _bodyEl!: HTMLDivElement;
|
|
2787
2856
|
private _loadingEl: HTMLElement | null = null;
|
|
2857
|
+
private _busyEl: HTMLElement | null = null;
|
|
2858
|
+
private _busyTextEl: HTMLElement | null = null;
|
|
2859
|
+
private _busyCount = 0;
|
|
2788
2860
|
private _errorEl: HTMLElement | null = null;
|
|
2789
2861
|
private _refreshBtn: HTMLButtonElement | null = null;
|
|
2790
2862
|
private _filterBtn: HTMLButtonElement | null = null;
|
package/style/base.css
CHANGED
|
@@ -559,6 +559,13 @@ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotHalo {
|
|
|
559
559
|
animation: none;
|
|
560
560
|
opacity: 0.5;
|
|
561
561
|
}
|
|
562
|
+
|
|
563
|
+
/* The click-time busy spinner is the only thing on screen while a click is
|
|
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. */
|
|
566
|
+
.jp-AiAssistantsPanel-busySpinner {
|
|
567
|
+
opacity: 1;
|
|
568
|
+
}
|
|
562
569
|
}
|
|
563
570
|
|
|
564
571
|
/* Full-panel veil shown only during an explicit refresh (refresh button or
|
|
@@ -581,6 +588,26 @@ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotHalo {
|
|
|
581
588
|
border-width: 3px;
|
|
582
589
|
}
|
|
583
590
|
|
|
591
|
+
/* Click-time busy overlay (DEF-PANE-318): raised on the click, before the
|
|
592
|
+
terminal probe or the branch request. No pointer events and no focus - the
|
|
593
|
+
context menu that launched a branch stays open until its command runs, and
|
|
594
|
+
a focus change would close it. */
|
|
595
|
+
.jp-AiAssistantsPanel-busyOverlay:not([hidden]) {
|
|
596
|
+
position: absolute;
|
|
597
|
+
inset: 0;
|
|
598
|
+
z-index: 9;
|
|
599
|
+
display: flex;
|
|
600
|
+
align-items: center;
|
|
601
|
+
justify-content: center;
|
|
602
|
+
pointer-events: none;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.jp-AiAssistantsPanel-busySpinner {
|
|
606
|
+
width: 24px;
|
|
607
|
+
height: 24px;
|
|
608
|
+
border-width: 3px;
|
|
609
|
+
}
|
|
610
|
+
|
|
584
611
|
.jp-AiAssistantsPanel-launchOverlay {
|
|
585
612
|
display: flex;
|
|
586
613
|
flex-direction: column;
|
|
@@ -740,7 +767,7 @@ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotHalo {
|
|
|
740
767
|
border-radius: var(--aica-radius-lg);
|
|
741
768
|
}
|
|
742
769
|
|
|
743
|
-
/* Visually hidden polite live region (
|
|
770
|
+
/* Visually hidden polite live region (polite announcements). */
|
|
744
771
|
.jp-AiAssistantsPanel-srOnly {
|
|
745
772
|
position: absolute;
|
|
746
773
|
width: 1px;
|