jupyterlab_ai_code_assistants_extension 1.2.282 → 1.2.294
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 +23 -1
- package/lib/core/panel.d.ts +14 -7
- package/lib/core/panel.js +215 -37
- package/lib/core/popup.js +1 -1
- package/lib/core/request.d.ts +5 -0
- package/lib/core/request.js +15 -0
- package/lib/core/types.d.ts +6 -2
- package/lib/index.js +109 -19
- package/lib/providers/claude.js +1 -1
- package/lib/providers/codex.js +1 -1
- package/lib/providers/deepseek.js +1 -1
- package/lib/providers/gemini.js +1 -1
- package/lib/providers/kimi.js +1 -1
- package/package.json +1 -1
- package/schema/plugin.json +5 -5
- package/src/__tests__/index.spec.ts +276 -30
- package/src/__tests__/launcher.spec.ts +3 -1
- package/src/__tests__/panel.spec.ts +575 -10
- package/src/__tests__/popup.spec.ts +2 -2
- package/src/__tests__/request.spec.ts +25 -1
- package/src/core/panel.ts +238 -43
- package/src/core/popup.ts +1 -1
- package/src/core/request.ts +17 -0
- package/src/core/types.ts +6 -2
- package/src/index.ts +115 -20
- package/src/providers/claude.ts +1 -1
- package/src/providers/codex.ts +1 -1
- package/src/providers/deepseek.ts +1 -1
- package/src/providers/gemini.ts +1 -1
- package/src/providers/kimi.ts +1 -1
- package/style/base.css +16 -3
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
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. One install replaces the separate `jupyterlab_claude_code_extension`, `jupyterlab_codex_extension` and `jupyterlab_kimi_code_extension` packages and migrates their settings and favourites automatically.
|
|
12
12
|
|
|
13
|
-

|
|
14
14
|
|
|
15
15
|
## Why this extension
|
|
16
16
|
|
|
@@ -36,6 +36,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
36
36
|
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id. "Manage Sessions..." opens a searchable list with multi-select delete and copy-id
|
|
37
37
|
- **Branch session** - fork the current conversation into a new named session from the right-click menu
|
|
38
38
|
- **Rename session** - name a conversation from the right-click menu, and the panel shows the name the assistant itself kept
|
|
39
|
+
- **Command line** - `jupyterlab-ai-code-assistants` lists, renames, moves, deletes, branches and switches the sessions of every assistant and refreshes the panels, from a terminal or an agent; every open panel shows the change at once
|
|
39
40
|
- **Launch modes under each assistant's own name** - Claude's skip-permissions, Codex's approval bypass, Kimi's `--yolo` and Gemini's YOLO; unsafe variants carry a shield glyph
|
|
40
41
|
- **Coloured terminal tabs** - each session's colour tints its terminal tab, and a branched session inherits its parent's colour. Claude's `/color` supplies a default and Kimi derives a stable one; Codex, Gemini and DeepSeek have no colour until you set one
|
|
41
42
|
- **Your newest colour choice wins** - a colour set on a terminal tab is remembered for that conversation; a Kimi or DeepSeek conversation that has never been resumed, including one started with `+`, is the exception, because its id cannot be read yet. In Claude Code whichever of the tab colour and `/color` you used last wins, and Reset Tab Colour in the right-click menu hands back the colours you set by hand
|
|
@@ -76,6 +77,27 @@ pip install jupyterlab_ai_code_assistants_extension
|
|
|
76
77
|
> [!WARNING]
|
|
77
78
|
> `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
79
|
|
|
80
|
+
## Command line
|
|
81
|
+
|
|
82
|
+
The wheel installs `jupyterlab-ai-code-assistants`. It finds the running Jupyter server and its token, and changes sessions only through that server, so every open panel shows the change at once.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
jupyterlab-ai-code-assistants # the server and every assistant (status)
|
|
86
|
+
jupyterlab-ai-code-assistants sessions list -a claude -p ~/project
|
|
87
|
+
jupyterlab-ai-code-assistants sessions rename <id> "New name" -a kimi
|
|
88
|
+
jupyterlab-ai-code-assistants sessions move <id> --to ~/other-project -a gemini
|
|
89
|
+
jupyterlab-ai-code-assistants sessions delete <id> -a claude
|
|
90
|
+
jupyterlab-ai-code-assistants sessions branch <id> -a claude --name "Experiment"
|
|
91
|
+
jupyterlab-ai-code-assistants sessions switch <id> -a codex
|
|
92
|
+
jupyterlab-ai-code-assistants refresh # after installing or removing an assistant CLI
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- `--url` and `--json` go before the subcommand; `--url` chooses the server when several run
|
|
96
|
+
- There is no token option: the token comes from `JUPYTERLAB_AI_CODE_ASSISTANTS_TOKEN`, else the server's runtime record, else `JUPYTERHUB_API_TOKEN`, `JPY_API_TOKEN` or `JUPYTER_TOKEN` for a loopback address
|
|
97
|
+
- A session a running terminal holds is never moved or deleted, and a project's current session is never deleted
|
|
98
|
+
- A Codex move forks the session into the target under a new id, which the command prints, and archives the source
|
|
99
|
+
- `jupyterlab-ai-code-assistants --help` lists the exit codes; `--help` after a subcommand lists its options
|
|
100
|
+
|
|
79
101
|
## Agent skill
|
|
80
102
|
|
|
81
103
|
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.
|
package/lib/core/panel.d.ts
CHANGED
|
@@ -87,13 +87,9 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
87
87
|
* the launch REACHED the server and was refused, and nothing retries it. */
|
|
88
88
|
private _notifyLaunchError;
|
|
89
89
|
private _logError;
|
|
90
|
+
/** `keepSame`: an answer equal to what is drawn leaves the rows in place. */
|
|
90
91
|
private _fetch;
|
|
91
92
|
private _toggleFavourite;
|
|
92
|
-
/** The warning a deletion earns: with the trash setting on, a refused move
|
|
93
|
-
* may remove the conversations permanently or leave them in place, so
|
|
94
|
-
* neither the verb nor the note may promise one outcome. */
|
|
95
|
-
private get _irreversibleNote();
|
|
96
|
-
private get _disposalVerb();
|
|
97
93
|
private _removeProject;
|
|
98
94
|
private _cleanupParallel;
|
|
99
95
|
/** This provider's launch-mode token for one action - see
|
|
@@ -316,6 +312,13 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
316
312
|
* all. Returning null lets the popup fall back to its fork marker. */
|
|
317
313
|
private _bgBadgeFor;
|
|
318
314
|
private _startPolling;
|
|
315
|
+
/** The server says this assistant's sessions changed (ACC-CLI-227): read
|
|
316
|
+
* them through the poll's single-flight fetch. Unlike a poll tick it is
|
|
317
|
+
* never dropped. It waits for an open row context menu to close, and one
|
|
318
|
+
* that lands while a read is in flight reads once more after it, because
|
|
319
|
+
* that read may have been answered before the change. */
|
|
320
|
+
sessionsChanged(): void;
|
|
321
|
+
private _poll;
|
|
319
322
|
private _stopPolling;
|
|
320
323
|
private readonly _app;
|
|
321
324
|
private readonly _descriptor;
|
|
@@ -375,9 +378,13 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
375
378
|
private _activeSession;
|
|
376
379
|
private _activeRowEl;
|
|
377
380
|
private _pollHandle;
|
|
378
|
-
/** The poll's own in-flight tick. Only the poll
|
|
379
|
-
* Refresh must always issue, however long the last poll is
|
|
381
|
+
/** The poll's own in-flight tick. Only the poll and a pushed change read it
|
|
382
|
+
* - an explicit Refresh must always issue, however long the last poll is
|
|
383
|
+
* taking. */
|
|
380
384
|
private _polling;
|
|
385
|
+
/** A pushed change waits for the context menu to close or for the read in
|
|
386
|
+
* flight to answer - see `sessionsChanged`. */
|
|
387
|
+
private _changeWaits;
|
|
381
388
|
/** Listing requests sent, and the number of the newest one drawn - see
|
|
382
389
|
* `_fetch`. */
|
|
383
390
|
private _fetchSent;
|
package/lib/core/panel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Clipboard, Dialog, InputDialog, Notification
|
|
1
|
+
import { Clipboard, Dialog, InputDialog, Notification } from '@jupyterlab/apputils';
|
|
2
2
|
import { MenuSvg, closeIcon, copyIcon, editIcon, folderIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
3
3
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
4
4
|
import { CommandRegistry } from '@lumino/commands';
|
|
@@ -40,6 +40,18 @@ const INLINE_BRANCH_LIMIT = 5;
|
|
|
40
40
|
* exports no token, so its absence is something to ask the command registry
|
|
41
41
|
* about at click time rather than a build-time dependency (ACC-LNCH-155). */
|
|
42
42
|
const BASIC_TERMINAL_LAUNCH = 'basic-terminal:launch';
|
|
43
|
+
/** What a delete says about the conversations the server kept because a live
|
|
44
|
+
* terminal runs them (ACC-SESS-57); empty when it kept none. */
|
|
45
|
+
function keptCopy(liveIds) {
|
|
46
|
+
var _a;
|
|
47
|
+
const kept = (_a = liveIds === null || liveIds === void 0 ? void 0 : liveIds.length) !== null && _a !== void 0 ? _a : 0;
|
|
48
|
+
if (kept === 0) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
return kept === 1
|
|
52
|
+
? 'One conversation was kept, because a live terminal runs it.'
|
|
53
|
+
: `${kept} conversations were kept, because live terminals run them.`;
|
|
54
|
+
}
|
|
43
55
|
export const DEFAULT_PRESENTATION_MODE = 'name';
|
|
44
56
|
const SECTION_LABELS = {
|
|
45
57
|
favourites: 'Favorites',
|
|
@@ -173,9 +185,13 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
173
185
|
this._activeSession = null;
|
|
174
186
|
this._activeRowEl = null;
|
|
175
187
|
this._pollHandle = null;
|
|
176
|
-
/** The poll's own in-flight tick. Only the poll
|
|
177
|
-
* Refresh must always issue, however long the last poll is
|
|
188
|
+
/** The poll's own in-flight tick. Only the poll and a pushed change read it
|
|
189
|
+
* - an explicit Refresh must always issue, however long the last poll is
|
|
190
|
+
* taking. */
|
|
178
191
|
this._polling = null;
|
|
192
|
+
/** A pushed change waits for the context menu to close or for the read in
|
|
193
|
+
* flight to answer - see `sessionsChanged`. */
|
|
194
|
+
this._changeWaits = false;
|
|
179
195
|
/** Listing requests sent, and the number of the newest one drawn - see
|
|
180
196
|
* `_fetch`. */
|
|
181
197
|
this._fetchSent = 0;
|
|
@@ -633,7 +649,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
633
649
|
console.error(`${LOG_PREFIX}[${this._descriptor.id}]`, message);
|
|
634
650
|
}
|
|
635
651
|
// ------------------------------------------------------------------- data
|
|
636
|
-
|
|
652
|
+
/** `keepSame`: an answer equal to what is drawn leaves the rows in place. */
|
|
653
|
+
async _fetch(keepSame = false) {
|
|
637
654
|
var _a;
|
|
638
655
|
// This request's place in the order. Requests answer in any order, and an
|
|
639
656
|
// answer older than the one already drawn is dropped (DEF-PANE-344).
|
|
@@ -647,10 +664,16 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
647
664
|
this._fetchDrawn = request;
|
|
648
665
|
// A project removed at or after this listing was sent must not be drawn
|
|
649
666
|
// back from the answer (DEF-PANE-351-WRITE-ORDER).
|
|
650
|
-
|
|
667
|
+
const sessions = ((_a = data.sessions) !== null && _a !== void 0 ? _a : []).filter(s => { var _a; return ((_a = this._removedAt.get(s.encoded_path)) !== null && _a !== void 0 ? _a : 0) < request; });
|
|
651
668
|
this._setInlineError(null);
|
|
652
|
-
|
|
653
|
-
|
|
669
|
+
// A redraw replaces every row, and the row a click is on with it
|
|
670
|
+
// (DEF-PANE-370-OWN-PUSH).
|
|
671
|
+
if (!keepSame ||
|
|
672
|
+
JSON.stringify(sessions) !== JSON.stringify(this._sessions)) {
|
|
673
|
+
this._sessions = sessions;
|
|
674
|
+
this._terminals.setSessions(this._sessions);
|
|
675
|
+
this._render();
|
|
676
|
+
}
|
|
654
677
|
// Best-effort tinting; never let a colour failure break the fetch.
|
|
655
678
|
void this._colours.load().then(() => this._terminals.reconcileColours());
|
|
656
679
|
}
|
|
@@ -668,8 +691,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
668
691
|
});
|
|
669
692
|
// A listing sent before the write may still be drawn, and a poll may
|
|
670
693
|
// have replaced the row this wrote; this one is answered after the write
|
|
671
|
-
// (DEF-PANE-351).
|
|
672
|
-
|
|
694
|
+
// (DEF-PANE-351). When it answers what the star above drew, the rows
|
|
695
|
+
// stay: the star is redrawn once.
|
|
696
|
+
void this._fetch(true).catch(() => undefined);
|
|
673
697
|
}
|
|
674
698
|
catch (err) {
|
|
675
699
|
session.favourite = !next; // roll back
|
|
@@ -677,27 +701,33 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
677
701
|
this._showActionError('Could not toggle favorite - try again.', err);
|
|
678
702
|
}
|
|
679
703
|
}
|
|
680
|
-
/** The warning a deletion earns: with the trash setting on, a refused move
|
|
681
|
-
* may remove the conversations permanently or leave them in place, so
|
|
682
|
-
* neither the verb nor the note may promise one outcome. */
|
|
683
|
-
get _irreversibleNote() {
|
|
684
|
-
return this._deleteToTrash
|
|
685
|
-
? ' A permanent removal cannot be undone.'
|
|
686
|
-
: ' This cannot be undone.';
|
|
687
|
-
}
|
|
688
|
-
get _disposalVerb() {
|
|
689
|
-
return this._deleteToTrash
|
|
690
|
-
? 'moved to the trash when the system accepts them; a refused move may remove them permanently or leave them in place'
|
|
691
|
-
: 'deleted permanently';
|
|
692
|
-
}
|
|
693
704
|
async _removeProject(session) {
|
|
694
705
|
var _a;
|
|
695
706
|
const name = this._lookupName(session);
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
707
|
+
const title = `Remove from ${this._descriptor.label}`;
|
|
708
|
+
// The consequence only, in two lines: the question and the outcome, then
|
|
709
|
+
// the permanent-deletion refusal on its own caution line
|
|
710
|
+
// (DEF-PANE-371-DIALOG-TEXT).
|
|
711
|
+
const outcome = this._deleteToTrash
|
|
712
|
+
? `Remove "${name}"? Every conversation in it goes to the trash.`
|
|
713
|
+
: `Remove "${name}"? Every conversation in it is deleted permanently.`;
|
|
714
|
+
const caution = this._deleteToTrash
|
|
715
|
+
? 'A refused move deletes permanently.'
|
|
716
|
+
: '';
|
|
717
|
+
const body = new Widget();
|
|
718
|
+
body.node.className = 'jp-AiAssistantsPanel-removeConfirm';
|
|
719
|
+
const question = document.createElement('p');
|
|
720
|
+
question.textContent = outcome;
|
|
721
|
+
body.node.appendChild(question);
|
|
722
|
+
if (caution) {
|
|
723
|
+
const line = document.createElement('p');
|
|
724
|
+
line.className = 'jp-AiAssistantsPanel-removeCaution';
|
|
725
|
+
line.textContent = caution;
|
|
726
|
+
body.node.appendChild(line);
|
|
727
|
+
}
|
|
728
|
+
const dialog = new Dialog({
|
|
729
|
+
title,
|
|
730
|
+
body,
|
|
701
731
|
buttons: [
|
|
702
732
|
Dialog.cancelButton(),
|
|
703
733
|
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
@@ -710,6 +740,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
710
740
|
defaultButton: 0,
|
|
711
741
|
renderer: dialogRenderer
|
|
712
742
|
});
|
|
743
|
+
// A Widget body leaves the Dialog no string to name itself from.
|
|
744
|
+
dialog.node.setAttribute('aria-label', [title, outcome, caution].filter(part => part).join(' '));
|
|
745
|
+
const confirm = await dialog.launch();
|
|
713
746
|
if (!confirm.button.accept) {
|
|
714
747
|
return;
|
|
715
748
|
}
|
|
@@ -768,7 +801,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
768
801
|
caution.className = 'jp-AiAssistantsPanel-cleanupCaution';
|
|
769
802
|
caution.textContent = this._deleteToTrash
|
|
770
803
|
? 'A refused trash move deletes permanently and cannot be undone.'
|
|
771
|
-
:
|
|
804
|
+
: 'This cannot be undone.';
|
|
772
805
|
confirmBody.node.appendChild(caution);
|
|
773
806
|
const title = this._trans.__('Clean Up Parallel Sessions');
|
|
774
807
|
let warning = null;
|
|
@@ -783,9 +816,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
783
816
|
defaultButton: 0,
|
|
784
817
|
renderer: dialogRenderer
|
|
785
818
|
});
|
|
786
|
-
//
|
|
787
|
-
|
|
788
|
-
//
|
|
819
|
+
// Scopes the armed style in base.css to this one dialog.
|
|
820
|
+
confirmDialog.addClass('jp-AiAssistantsPanel-cleanupDialog');
|
|
821
|
+
// The Dialog names itself only when its body is a string, and this body is
|
|
822
|
+
// a Widget: name it here, and again when the question is restated and the
|
|
823
|
+
// warning joins it.
|
|
789
824
|
const nameDialog = () => {
|
|
790
825
|
var _a, _b, _c, _d;
|
|
791
826
|
confirmDialog.node.setAttribute('aria-label', [
|
|
@@ -799,6 +834,105 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
799
834
|
.join(' '));
|
|
800
835
|
};
|
|
801
836
|
nameDialog();
|
|
837
|
+
// The house two-step destructive action (design-language.md, "Two-step
|
|
838
|
+
// destructive action"; the branch popup's Delete is the shipped form):
|
|
839
|
+
// while the newer state is unknown or newer, the accept button arms on the
|
|
840
|
+
// first activation and removes on the second, the arm lapsing after 4 s.
|
|
841
|
+
// It starts required: the branch fetch that decides whether a removal is
|
|
842
|
+
// safe is still out when the dialog opens.
|
|
843
|
+
let armRequired = true;
|
|
844
|
+
let armed = false;
|
|
845
|
+
let armTimer = 0;
|
|
846
|
+
// The arming activation's event.timeStamp, which the confirming one is
|
|
847
|
+
// measured against.
|
|
848
|
+
let armedAt = 0;
|
|
849
|
+
/** Find and relabel the accept button. The Dialog keeps its button nodes
|
|
850
|
+
* private and puts the label in a child whose aria-label overrides its
|
|
851
|
+
* text, so both are set. */
|
|
852
|
+
const updateAccept = () => {
|
|
853
|
+
const button = confirmDialog.node.querySelector('.jp-mod-accept');
|
|
854
|
+
const label = button === null || button === void 0 ? void 0 : button.querySelector('.jp-Dialog-buttonLabel');
|
|
855
|
+
if (!button || !label) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const text = armed
|
|
859
|
+
? this._trans.__('Confirm remove %1', extra)
|
|
860
|
+
: armRequired
|
|
861
|
+
? this._trans.__('Remove (%1)', extra)
|
|
862
|
+
: this._trans.__('Remove');
|
|
863
|
+
label.textContent = text;
|
|
864
|
+
label.setAttribute('aria-label', text);
|
|
865
|
+
button.classList.toggle('jp-mod-armed', armed);
|
|
866
|
+
};
|
|
867
|
+
const disarm = () => {
|
|
868
|
+
armed = false;
|
|
869
|
+
window.clearTimeout(armTimer);
|
|
870
|
+
};
|
|
871
|
+
/** Consume one accept activation. True means it only armed, or was the
|
|
872
|
+
* ignored half of a double-click, and the Dialog must not resolve; false
|
|
873
|
+
* lets the Dialog's own handler resolve it. */
|
|
874
|
+
const activate = (event) => {
|
|
875
|
+
if (!armRequired) {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
// The second click of a double-click is not a confirmation (DEF-PANE-336):
|
|
879
|
+
// a multi-click within 600 ms of arming is ignored. A later click
|
|
880
|
+
// confirms whatever its detail, since the OS double-click interval can
|
|
881
|
+
// be longer than the user's pause (DEF-PANE-338).
|
|
882
|
+
if (armed && event.detail > 1 && event.timeStamp - armedAt < 600) {
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
if (!armed) {
|
|
886
|
+
armed = true;
|
|
887
|
+
armedAt = event.timeStamp;
|
|
888
|
+
window.clearTimeout(armTimer);
|
|
889
|
+
armTimer = window.setTimeout(() => {
|
|
890
|
+
disarm();
|
|
891
|
+
updateAccept();
|
|
892
|
+
}, 4000);
|
|
893
|
+
updateAccept();
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
disarm();
|
|
897
|
+
return false;
|
|
898
|
+
};
|
|
899
|
+
// Registered before launch(): the Dialog attaches its own capture
|
|
900
|
+
// listeners on this same node when launch() attaches it, so these run
|
|
901
|
+
// first and can swallow an activation that must only arm. Space arrives
|
|
902
|
+
// as a click; Enter arrives as a keydown the Dialog would otherwise
|
|
903
|
+
// resolve from, so both are intercepted here.
|
|
904
|
+
confirmDialog.node.addEventListener('click', event => {
|
|
905
|
+
const button = confirmDialog.node.querySelector('.jp-mod-accept');
|
|
906
|
+
if (!button || !button.contains(event.target)) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (activate(event)) {
|
|
910
|
+
event.stopImmediatePropagation();
|
|
911
|
+
event.preventDefault();
|
|
912
|
+
}
|
|
913
|
+
}, true);
|
|
914
|
+
confirmDialog.node.addEventListener('keydown', event => {
|
|
915
|
+
if (event.key !== 'Enter') {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const accept = confirmDialog.node.querySelector('.jp-mod-accept');
|
|
919
|
+
if (!accept || document.activeElement !== accept) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
// A held key's auto-repeat is not a second press (DEF-PANE-347): it
|
|
923
|
+
// must neither confirm nor arm, and consuming it stops the Dialog's
|
|
924
|
+
// own Enter handler from resolving the accept.
|
|
925
|
+
if (event.repeat) {
|
|
926
|
+
event.stopImmediatePropagation();
|
|
927
|
+
event.preventDefault();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (activate(event)) {
|
|
931
|
+
event.stopImmediatePropagation();
|
|
932
|
+
event.preventDefault();
|
|
933
|
+
}
|
|
934
|
+
}, true);
|
|
935
|
+
updateAccept();
|
|
802
936
|
// Alongside the question, never in front of it: the request layer allows
|
|
803
937
|
// 60 s, and the confirmation must open at once. When the list lands the
|
|
804
938
|
// question states its length and a conversation newer than the kept one
|
|
@@ -830,6 +964,13 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
830
964
|
'active after the one kept.';
|
|
831
965
|
confirmBody.node.appendChild(warning);
|
|
832
966
|
}
|
|
967
|
+
// The list just decided what the button acts on: an arm given while
|
|
968
|
+
// it was pending could confirm a different set, so drop it and
|
|
969
|
+
// restate the label with the fetched count. With no newer branch the
|
|
970
|
+
// button returns to the one-click Remove.
|
|
971
|
+
disarm();
|
|
972
|
+
armRequired = newer > 0;
|
|
973
|
+
updateAccept();
|
|
833
974
|
nameDialog();
|
|
834
975
|
}
|
|
835
976
|
return fetched;
|
|
@@ -899,7 +1040,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
899
1040
|
// The kept listing names conversations that may have just gone.
|
|
900
1041
|
this._branchCache = null;
|
|
901
1042
|
bar.value = 1;
|
|
902
|
-
message.textContent =
|
|
1043
|
+
message.textContent = [
|
|
1044
|
+
`Removed ${data.removed_count} parallel session${data.removed_count === 1 ? '' : 's'}.`,
|
|
1045
|
+
keptCopy(data.live_ids)
|
|
1046
|
+
]
|
|
1047
|
+
.filter(part => part)
|
|
1048
|
+
.join(' ');
|
|
903
1049
|
// Refresh so the row's conversation count and menu label update.
|
|
904
1050
|
await this._fetch();
|
|
905
1051
|
}
|
|
@@ -1502,6 +1648,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1502
1648
|
await this._colours.forget((_a = result.removed_ids) !== null && _a !== void 0 ? _a : sessionIds);
|
|
1503
1649
|
// The kept listing names conversations that may have just gone.
|
|
1504
1650
|
this._branchCache = null;
|
|
1651
|
+
const kept = keptCopy(result.live_ids);
|
|
1652
|
+
if (kept) {
|
|
1653
|
+
Notification.info(kept, { autoClose: 4000 });
|
|
1654
|
+
}
|
|
1505
1655
|
return result.removed_count;
|
|
1506
1656
|
}
|
|
1507
1657
|
catch (err) {
|
|
@@ -2420,6 +2570,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2420
2570
|
// callback still needs to read it. The field is overwritten on the next
|
|
2421
2571
|
// contextmenu open.
|
|
2422
2572
|
this._setActiveRow(null);
|
|
2573
|
+
// A change pushed while the menu was open reads now. A microtask later,
|
|
2574
|
+
// because the menu is still attached while this signal fires.
|
|
2575
|
+
if (this._changeWaits) {
|
|
2576
|
+
queueMicrotask(() => this.sessionsChanged());
|
|
2577
|
+
}
|
|
2423
2578
|
});
|
|
2424
2579
|
}
|
|
2425
2580
|
_menu(commands = this._commands) {
|
|
@@ -2698,13 +2853,36 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2698
2853
|
if (this._polling !== null) {
|
|
2699
2854
|
return;
|
|
2700
2855
|
}
|
|
2701
|
-
this.
|
|
2702
|
-
.catch(err => this._showError(err))
|
|
2703
|
-
.finally(() => {
|
|
2704
|
-
this._polling = null;
|
|
2705
|
-
});
|
|
2856
|
+
this._poll();
|
|
2706
2857
|
}, POLL_INTERVAL_MS);
|
|
2707
2858
|
}
|
|
2859
|
+
/** The server says this assistant's sessions changed (ACC-CLI-227): read
|
|
2860
|
+
* them through the poll's single-flight fetch. Unlike a poll tick it is
|
|
2861
|
+
* never dropped. It waits for an open row context menu to close, and one
|
|
2862
|
+
* that lands while a read is in flight reads once more after it, because
|
|
2863
|
+
* that read may have been answered before the change. */
|
|
2864
|
+
sessionsChanged() {
|
|
2865
|
+
if (this.isDisposed) {
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
if (this._contextMenu.isAttached || this._polling !== null) {
|
|
2869
|
+
this._changeWaits = true;
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
this._poll();
|
|
2873
|
+
}
|
|
2874
|
+
_poll() {
|
|
2875
|
+
// A read sent now answers every change pushed before it.
|
|
2876
|
+
this._changeWaits = false;
|
|
2877
|
+
this._polling = this._fetch()
|
|
2878
|
+
.catch(err => this._showError(err))
|
|
2879
|
+
.finally(() => {
|
|
2880
|
+
this._polling = null;
|
|
2881
|
+
if (this._changeWaits) {
|
|
2882
|
+
this.sessionsChanged();
|
|
2883
|
+
}
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2708
2886
|
_stopPolling() {
|
|
2709
2887
|
if (this._pollHandle !== null) {
|
|
2710
2888
|
window.clearInterval(this._pollHandle);
|
package/lib/core/popup.js
CHANGED
|
@@ -91,7 +91,7 @@ export function showManageSessionsPopup(options) {
|
|
|
91
91
|
deleteBtn.type = 'button';
|
|
92
92
|
deleteBtn.className = 'jp-AiAssistantsPanel-branchDelete';
|
|
93
93
|
deleteBtn.title = options.deleteToTrash
|
|
94
|
-
? 'Deleted conversations
|
|
94
|
+
? 'Deleted conversations go to the trash; a refused move deletes permanently.'
|
|
95
95
|
: 'Deleted conversations are removed permanently';
|
|
96
96
|
footer.appendChild(deleteBtn);
|
|
97
97
|
// Plain visual counter (selection, or the last action), beside the button
|
package/lib/core/request.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { ServerConnection } from '@jupyterlab/services';
|
|
2
2
|
/** The server extension's API namespace. Every route lives under it. */
|
|
3
3
|
export declare const API_NAMESPACE = "jupyterlab-ai-code-assistants-extension";
|
|
4
|
+
/** This window's name, sent with every request in {@link WINDOW_HEADER}. The
|
|
5
|
+
* server names it in the push a change causes, and this window skips that
|
|
6
|
+
* push: its panel re-reads after every write it makes (DEF-PANE-370-OWN-PUSH). */
|
|
7
|
+
export declare const WINDOW_ID: string;
|
|
8
|
+
export declare const WINDOW_HEADER = "X-AI-Assistants-Window";
|
|
4
9
|
/**
|
|
5
10
|
* DEF-72: how long a single server call may run before the client gives up.
|
|
6
11
|
*
|
package/lib/core/request.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { URLExt } from '@jupyterlab/coreutils';
|
|
2
2
|
import { ServerConnection } from '@jupyterlab/services';
|
|
3
|
+
import { UUID } from '@lumino/coreutils';
|
|
3
4
|
import { LOG_PREFIX } from './registry';
|
|
4
5
|
/** The server extension's API namespace. Every route lives under it. */
|
|
5
6
|
export const API_NAMESPACE = 'jupyterlab-ai-code-assistants-extension';
|
|
7
|
+
/** This window's name, sent with every request in {@link WINDOW_HEADER}. The
|
|
8
|
+
* server names it in the push a change causes, and this window skips that
|
|
9
|
+
* push: its panel re-reads after every write it makes (DEF-PANE-370-OWN-PUSH). */
|
|
10
|
+
export const WINDOW_ID = UUID.uuid4();
|
|
11
|
+
export const WINDOW_HEADER = 'X-AI-Assistants-Window';
|
|
6
12
|
/**
|
|
7
13
|
* DEF-72: how long a single server call may run before the client gives up.
|
|
8
14
|
*
|
|
@@ -68,6 +74,15 @@ export function isRequestTimeout(err) {
|
|
|
68
74
|
export async function requestAPI(endPoint, serverSettings, init = {}) {
|
|
69
75
|
var _a, _b;
|
|
70
76
|
const requestUrl = URLExt.join(serverSettings.baseUrl, API_NAMESPACE, endPoint);
|
|
77
|
+
// shortcut: a `Headers` instance in `init.headers` would be dropped here; no
|
|
78
|
+
// caller passes one.
|
|
79
|
+
init = {
|
|
80
|
+
...init,
|
|
81
|
+
headers: {
|
|
82
|
+
...init.headers,
|
|
83
|
+
[WINDOW_HEADER]: WINDOW_ID
|
|
84
|
+
}
|
|
85
|
+
};
|
|
71
86
|
// DEF-72: `ServerConnection.makeRequest` sets no deadline of its own, so
|
|
72
87
|
// without this a wedged server holds the call for as long as the browser
|
|
73
88
|
// will hold the socket. Hand-rolled rather than `AbortSignal.timeout`, which
|
package/lib/core/types.d.ts
CHANGED
|
@@ -163,8 +163,12 @@ export interface IDisposalReport {
|
|
|
163
163
|
removed_ids: string[];
|
|
164
164
|
}
|
|
165
165
|
export type IRemoveResponse = IDisposalReport;
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
/** A delete of chosen conversations also names the ones it kept because a
|
|
167
|
+
* live terminal runs them (ACC-SESS-57). */
|
|
168
|
+
export interface IDeleteSessionsResponse extends IDisposalReport {
|
|
169
|
+
live_ids: string[];
|
|
170
|
+
}
|
|
171
|
+
export type ICleanupResponse = IDeleteSessionsResponse;
|
|
168
172
|
export interface IBranch {
|
|
169
173
|
session_id: string;
|
|
170
174
|
file_mtime: number;
|