jupyterlab_ai_code_assistants_extension 1.2.271 → 1.2.288
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 +46 -24
- package/lib/core/panel.d.ts +14 -2
- package/lib/core/panel.js +173 -25
- 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 +471 -12
- package/src/__tests__/request.spec.ts +25 -1
- package/src/core/panel.ts +190 -26
- 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 +29 -0
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
|
|
|
@@ -25,30 +25,31 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
28
|
-
- **One install, every assistant** - Claude Code, Codex, Kimi, Gemini and DeepSeek
|
|
29
|
-
- **Provider registry** - assistant-specific behaviour lives in one module per assistant
|
|
30
|
-
- **Joint settings page** - one settings section
|
|
31
|
-
- **Three-section side panel** - Favorites, Recent
|
|
32
|
-
- **One-click resume** - click a row to
|
|
33
|
-
- **File browser follows the opened session** - opening a session moves the
|
|
34
|
-
- **Launcher tiles** -
|
|
35
|
-
- **Your terminal's `claude -c` follows the panel** (Claude) -
|
|
36
|
-
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id
|
|
37
|
-
- **Branch session** - fork the current conversation into a new named session
|
|
38
|
-
- **Rename session** - name a conversation from the right-click menu, and the
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
44
|
-
- **
|
|
45
|
-
- **
|
|
46
|
-
- **
|
|
28
|
+
- **One install, every assistant** - Claude Code, Codex, Kimi, Gemini and DeepSeek in one package, each with its own right-side panel and official mark
|
|
29
|
+
- **Provider registry** - assistant-specific behaviour lives in one module per assistant, so adding one changes no core file
|
|
30
|
+
- **Joint settings page** - one settings section covers all assistants with a per-assistant toggle, all on by default; toggling takes effect without a JupyterLab reload
|
|
31
|
+
- **Three-section side panel** - Favorites, Recent and All projects, each scrolling independently
|
|
32
|
+
- **One-click resume** - click a row to resume its session in a terminal, reusing an open terminal for the project. A Kimi or DeepSeek conversation started with `+` opens a second terminal when its first is still open
|
|
33
|
+
- **File browser follows the opened session** - opening a session moves the file browser to its 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
|
+
- **Launcher tiles** - each enabled assistant has a tile in the Launcher's AI Assistants section; a click opens the assistant in the file browser's current folder, resuming that folder's conversation when one exists
|
|
35
|
+
- **Your terminal's `claude -c` follows the panel** (Claude) - selecting a conversation row makes plain `claude -c` in that project resume the same conversation, even one that has been compacted
|
|
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
|
+
- **Branch session** - fork the current conversation into a new named session from the right-click menu
|
|
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
|
|
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
|
|
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
|
|
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
|
|
43
|
+
- **Favorites** - star projects from the right-click menu; favourites from the standalone extensions are migrated on first run
|
|
44
|
+
- **Remove and clean up** - remove a project's history or its extra parallel sessions from the right-click menu, with a confirmation. Removed files honour JupyterLab's "move files to trash" setting
|
|
45
|
+
- **Activity at a glance** - each row shows its last activity in an aligned column. Recent rows light up, stale rows dim, and parallel conversations show a branch icon and count
|
|
46
|
+
- **Remote control indicator and background agents** (Claude) - a green dot marks sessions under remote control, and a `bg` chip marks a conversation held by a background agent. Clicking stops the agent and resumes the conversation in your terminal
|
|
47
|
+
- **Search** - a fuzzy filter per panel, toggled by the funnel button
|
|
47
48
|
- **Presentation modes** - label rows by session name or by path relative to the JupyterLab root
|
|
48
|
-
- **Conflict-safe upgrade** - if a retired standalone extension is still installed, its panel wins and this extension stands down for that assistant
|
|
49
|
-
- **DeepSeek in the terminal** - the
|
|
50
|
-
- **Auto-disabled when absent** - an assistant whose CLI is not on `PATH`
|
|
51
|
-
- **Open Callback** - when an assistant's login
|
|
49
|
+
- **Conflict-safe upgrade** - if a retired standalone extension is still installed, its panel wins and this extension stands down for that assistant
|
|
50
|
+
- **DeepSeek in the terminal** - the panel runs the community TUI bundle, because the DeepSeek Harness ships no terminal chat. Without the bundle a launch is refused before a terminal opens
|
|
51
|
+
- **Auto-disabled when absent** - an assistant whose CLI is not on `PATH` shows no panel
|
|
52
|
+
- **Open Callback** - when an assistant's login redirects to a loopback address that will not load, run Open Callback from the command palette and paste the link. The server makes the request and says whether the login accepted it
|
|
52
53
|
|
|
53
54
|
## Requirements
|
|
54
55
|
|
|
@@ -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,6 +87,7 @@ 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
93
|
/** The warning a deletion earns: with the trash setting on, a refused move
|
|
@@ -316,6 +317,13 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
316
317
|
* all. Returning null lets the popup fall back to its fork marker. */
|
|
317
318
|
private _bgBadgeFor;
|
|
318
319
|
private _startPolling;
|
|
320
|
+
/** The server says this assistant's sessions changed (ACC-CLI-227): read
|
|
321
|
+
* them through the poll's single-flight fetch. Unlike a poll tick it is
|
|
322
|
+
* never dropped. It waits for an open row context menu to close, and one
|
|
323
|
+
* that lands while a read is in flight reads once more after it, because
|
|
324
|
+
* that read may have been answered before the change. */
|
|
325
|
+
sessionsChanged(): void;
|
|
326
|
+
private _poll;
|
|
319
327
|
private _stopPolling;
|
|
320
328
|
private readonly _app;
|
|
321
329
|
private readonly _descriptor;
|
|
@@ -375,9 +383,13 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
375
383
|
private _activeSession;
|
|
376
384
|
private _activeRowEl;
|
|
377
385
|
private _pollHandle;
|
|
378
|
-
/** The poll's own in-flight tick. Only the poll
|
|
379
|
-
* Refresh must always issue, however long the last poll is
|
|
386
|
+
/** The poll's own in-flight tick. Only the poll and a pushed change read it
|
|
387
|
+
* - an explicit Refresh must always issue, however long the last poll is
|
|
388
|
+
* taking. */
|
|
380
389
|
private _polling;
|
|
390
|
+
/** A pushed change waits for the context menu to close or for the read in
|
|
391
|
+
* flight to answer - see `sessionsChanged`. */
|
|
392
|
+
private _changeWaits;
|
|
381
393
|
/** Listing requests sent, and the number of the newest one drawn - see
|
|
382
394
|
* `_fetch`. */
|
|
383
395
|
private _fetchSent;
|
package/lib/core/panel.js
CHANGED
|
@@ -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
|
|
@@ -737,14 +761,44 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
737
761
|
}
|
|
738
762
|
}
|
|
739
763
|
async _cleanupParallel(session) {
|
|
740
|
-
var _a;
|
|
741
|
-
const extra = session.extra_sessions;
|
|
764
|
+
var _a, _b, _c, _d;
|
|
742
765
|
const name = this._lookupName(session);
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
766
|
+
const confirmBody = new Widget();
|
|
767
|
+
confirmBody.node.className = 'jp-AiAssistantsPanel-cleanupConfirm';
|
|
768
|
+
const question = document.createElement('p');
|
|
769
|
+
// Announced politely when the fetch restates it; the warning keeps
|
|
770
|
+
// role=alert for the case that needs attention.
|
|
771
|
+
question.setAttribute('role', 'status');
|
|
772
|
+
let extra = session.extra_sessions;
|
|
773
|
+
const askCount = (count) => {
|
|
774
|
+
question.textContent =
|
|
775
|
+
`Remove ${count} parallel session${count === 1 ? '' : 's'} from ` +
|
|
776
|
+
`"${name}"?`;
|
|
777
|
+
};
|
|
778
|
+
askCount(extra);
|
|
779
|
+
confirmBody.node.appendChild(question);
|
|
780
|
+
// One sentence, twelve words or fewer: what happens, not the detail.
|
|
781
|
+
const hint = document.createElement('p');
|
|
782
|
+
hint.className = 'jp-AiAssistantsPanel-cleanupHint';
|
|
783
|
+
hint.textContent = this._deleteToTrash
|
|
784
|
+
? 'The current conversation is kept; the rest go to the trash.'
|
|
785
|
+
: 'The current conversation is kept; the rest are deleted permanently.';
|
|
786
|
+
confirmBody.node.appendChild(hint);
|
|
787
|
+
// The irreversible note is a safety statement, not the hint, so it keeps
|
|
788
|
+
// its own line. With the trash on it names the refusal: a refused move
|
|
789
|
+
// deletes permanently, while the hint promises the trash
|
|
790
|
+
// (DEF-PANE-363-CAUTION-REFUSAL).
|
|
791
|
+
const caution = document.createElement('p');
|
|
792
|
+
caution.className = 'jp-AiAssistantsPanel-cleanupCaution';
|
|
793
|
+
caution.textContent = this._deleteToTrash
|
|
794
|
+
? 'A refused trash move deletes permanently and cannot be undone.'
|
|
795
|
+
: this._irreversibleNote.trim();
|
|
796
|
+
confirmBody.node.appendChild(caution);
|
|
797
|
+
const title = this._trans.__('Clean Up Parallel Sessions');
|
|
798
|
+
let warning = null;
|
|
799
|
+
const confirmDialog = new Dialog({
|
|
800
|
+
title,
|
|
801
|
+
body: confirmBody,
|
|
748
802
|
buttons: [
|
|
749
803
|
Dialog.cancelButton(),
|
|
750
804
|
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
@@ -753,6 +807,58 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
753
807
|
defaultButton: 0,
|
|
754
808
|
renderer: dialogRenderer
|
|
755
809
|
});
|
|
810
|
+
// showDialog names a dialog only when its body is a string, and this body
|
|
811
|
+
// is a Widget: name it here, and again when the question is restated and
|
|
812
|
+
// the warning joins it.
|
|
813
|
+
const nameDialog = () => {
|
|
814
|
+
var _a, _b, _c, _d;
|
|
815
|
+
confirmDialog.node.setAttribute('aria-label', [
|
|
816
|
+
title,
|
|
817
|
+
(_a = question.textContent) !== null && _a !== void 0 ? _a : '',
|
|
818
|
+
(_b = hint.textContent) !== null && _b !== void 0 ? _b : '',
|
|
819
|
+
(_c = caution.textContent) !== null && _c !== void 0 ? _c : '',
|
|
820
|
+
(_d = warning === null || warning === void 0 ? void 0 : warning.textContent) !== null && _d !== void 0 ? _d : ''
|
|
821
|
+
]
|
|
822
|
+
.filter(part => part)
|
|
823
|
+
.join(' '));
|
|
824
|
+
};
|
|
825
|
+
nameDialog();
|
|
826
|
+
// Alongside the question, never in front of it: the request layer allows
|
|
827
|
+
// 60 s, and the confirmation must open at once. When the list lands the
|
|
828
|
+
// question states its length and a conversation newer than the kept one
|
|
829
|
+
// adds the warning, drawn last so it reads after the caution. The same
|
|
830
|
+
// list is what the removal deletes. A failed fetch leaves the question as
|
|
831
|
+
// the row reported it, with no warning, and the removal fetches again
|
|
832
|
+
// below.
|
|
833
|
+
const branchesReady = this._fetchBranches(session).then(fetched => {
|
|
834
|
+
const list = fetched === null || fetched === void 0 ? void 0 : fetched.branches;
|
|
835
|
+
if (list && list.length === 0) {
|
|
836
|
+
// The row is up to one poll old, and a second client may have
|
|
837
|
+
// cleaned the project since (DEF-GUARD-96): a red Remove with zero
|
|
838
|
+
// targets is a dead end. End the confirmation through the accept
|
|
839
|
+
// path, whose zero-id guard states the empty outcome and refreshes
|
|
840
|
+
// the list (DEF-PANE-365); resolving after the user already
|
|
841
|
+
// accepted or cancelled is a no-op.
|
|
842
|
+
confirmDialog.resolve(1);
|
|
843
|
+
}
|
|
844
|
+
else if (list) {
|
|
845
|
+
extra = list.length;
|
|
846
|
+
askCount(extra);
|
|
847
|
+
const newer = list.filter(branch => branch.file_mtime > session.file_mtime).length;
|
|
848
|
+
if (newer > 0) {
|
|
849
|
+
warning = document.createElement('div');
|
|
850
|
+
warning.className = 'jp-AiAssistantsPanel-cleanupWarning';
|
|
851
|
+
warning.setAttribute('role', 'alert');
|
|
852
|
+
warning.textContent =
|
|
853
|
+
`${newer} parallel session${newer === 1 ? ' was' : 's were'} ` +
|
|
854
|
+
'active after the one kept.';
|
|
855
|
+
confirmBody.node.appendChild(warning);
|
|
856
|
+
}
|
|
857
|
+
nameDialog();
|
|
858
|
+
}
|
|
859
|
+
return fetched;
|
|
860
|
+
}, () => null);
|
|
861
|
+
const confirm = await confirmDialog.launch();
|
|
756
862
|
if (!confirm.button.accept) {
|
|
757
863
|
return;
|
|
758
864
|
}
|
|
@@ -760,6 +866,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
760
866
|
body.node.className = 'jp-AiAssistantsPanel-cleanupBody';
|
|
761
867
|
const message = document.createElement('div');
|
|
762
868
|
message.className = 'jp-AiAssistantsPanel-cleanupMessage';
|
|
869
|
+
message.setAttribute('role', 'status');
|
|
763
870
|
message.textContent = `Removing ${extra} parallel session${extra === 1 ? '' : 's'}...`;
|
|
764
871
|
body.node.appendChild(message);
|
|
765
872
|
// No `value` attribute -> indeterminate (animated) while the request is in
|
|
@@ -774,6 +881,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
774
881
|
buttons: [Dialog.okButton({ label: this._trans.__('Close') })],
|
|
775
882
|
renderer: dialogRenderer
|
|
776
883
|
});
|
|
884
|
+
dialog.node.setAttribute('aria-label', title);
|
|
777
885
|
// Close stays live while the work runs. Hiding it took away the only exit
|
|
778
886
|
// a keyboard user has: the dialog listens for Escape on its own node, it
|
|
779
887
|
// puts focus on this button and nowhere else, and focusing a hidden button
|
|
@@ -784,9 +892,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
784
892
|
void dialog.launch();
|
|
785
893
|
try {
|
|
786
894
|
// The extras ARE the branch list - the current conversation is not in
|
|
787
|
-
// it - so cleaning up is deleting exactly those ids.
|
|
788
|
-
|
|
789
|
-
|
|
895
|
+
// it - so cleaning up is deleting exactly those ids. Reuse the list the
|
|
896
|
+
// question was built from, waiting for it here where the progress
|
|
897
|
+
// dialog is already up; only a failed fetch re-reads.
|
|
898
|
+
const fetched = (_a = (await branchesReady)) !== null && _a !== void 0 ? _a : (await this._fetchBranches(session));
|
|
899
|
+
const ids = ((_b = fetched === null || fetched === void 0 ? void 0 : fetched.branches) !== null && _b !== void 0 ? _b : []).map(b => b.session_id);
|
|
790
900
|
if (ids.length === 0) {
|
|
791
901
|
// The menu item is gated on a row up to one poll old, so by the time
|
|
792
902
|
// the refetch lands another tab may have cleaned the project already.
|
|
@@ -795,6 +905,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
795
905
|
// to remove N.
|
|
796
906
|
bar.remove();
|
|
797
907
|
message.textContent = 'Nothing left to clean up.';
|
|
908
|
+
dialog.node.setAttribute('aria-label', `${title}. ${(_c = message.textContent) !== null && _c !== void 0 ? _c : ''}`);
|
|
798
909
|
this._branchCache = null;
|
|
799
910
|
await this._fetch();
|
|
800
911
|
return;
|
|
@@ -808,11 +919,16 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
808
919
|
});
|
|
809
920
|
// Only the conversations that ACTUALLY went lose their colour - one the
|
|
810
921
|
// server could not delete keeps its tint.
|
|
811
|
-
await this._colours.forget((
|
|
922
|
+
await this._colours.forget((_d = data.removed_ids) !== null && _d !== void 0 ? _d : ids);
|
|
812
923
|
// The kept listing names conversations that may have just gone.
|
|
813
924
|
this._branchCache = null;
|
|
814
925
|
bar.value = 1;
|
|
815
|
-
message.textContent =
|
|
926
|
+
message.textContent = [
|
|
927
|
+
`Removed ${data.removed_count} parallel session${data.removed_count === 1 ? '' : 's'}.`,
|
|
928
|
+
keptCopy(data.live_ids)
|
|
929
|
+
]
|
|
930
|
+
.filter(part => part)
|
|
931
|
+
.join(' ');
|
|
816
932
|
// Refresh so the row's conversation count and menu label update.
|
|
817
933
|
await this._fetch();
|
|
818
934
|
}
|
|
@@ -1415,6 +1531,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1415
1531
|
await this._colours.forget((_a = result.removed_ids) !== null && _a !== void 0 ? _a : sessionIds);
|
|
1416
1532
|
// The kept listing names conversations that may have just gone.
|
|
1417
1533
|
this._branchCache = null;
|
|
1534
|
+
const kept = keptCopy(result.live_ids);
|
|
1535
|
+
if (kept) {
|
|
1536
|
+
Notification.info(kept, { autoClose: 4000 });
|
|
1537
|
+
}
|
|
1418
1538
|
return result.removed_count;
|
|
1419
1539
|
}
|
|
1420
1540
|
catch (err) {
|
|
@@ -2333,6 +2453,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2333
2453
|
// callback still needs to read it. The field is overwritten on the next
|
|
2334
2454
|
// contextmenu open.
|
|
2335
2455
|
this._setActiveRow(null);
|
|
2456
|
+
// A change pushed while the menu was open reads now. A microtask later,
|
|
2457
|
+
// because the menu is still attached while this signal fires.
|
|
2458
|
+
if (this._changeWaits) {
|
|
2459
|
+
queueMicrotask(() => this.sessionsChanged());
|
|
2460
|
+
}
|
|
2336
2461
|
});
|
|
2337
2462
|
}
|
|
2338
2463
|
_menu(commands = this._commands) {
|
|
@@ -2611,13 +2736,36 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2611
2736
|
if (this._polling !== null) {
|
|
2612
2737
|
return;
|
|
2613
2738
|
}
|
|
2614
|
-
this.
|
|
2615
|
-
.catch(err => this._showError(err))
|
|
2616
|
-
.finally(() => {
|
|
2617
|
-
this._polling = null;
|
|
2618
|
-
});
|
|
2739
|
+
this._poll();
|
|
2619
2740
|
}, POLL_INTERVAL_MS);
|
|
2620
2741
|
}
|
|
2742
|
+
/** The server says this assistant's sessions changed (ACC-CLI-227): read
|
|
2743
|
+
* them through the poll's single-flight fetch. Unlike a poll tick it is
|
|
2744
|
+
* never dropped. It waits for an open row context menu to close, and one
|
|
2745
|
+
* that lands while a read is in flight reads once more after it, because
|
|
2746
|
+
* that read may have been answered before the change. */
|
|
2747
|
+
sessionsChanged() {
|
|
2748
|
+
if (this.isDisposed) {
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
if (this._contextMenu.isAttached || this._polling !== null) {
|
|
2752
|
+
this._changeWaits = true;
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
this._poll();
|
|
2756
|
+
}
|
|
2757
|
+
_poll() {
|
|
2758
|
+
// A read sent now answers every change pushed before it.
|
|
2759
|
+
this._changeWaits = false;
|
|
2760
|
+
this._polling = this._fetch()
|
|
2761
|
+
.catch(err => this._showError(err))
|
|
2762
|
+
.finally(() => {
|
|
2763
|
+
this._polling = null;
|
|
2764
|
+
if (this._changeWaits) {
|
|
2765
|
+
this.sessionsChanged();
|
|
2766
|
+
}
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2621
2769
|
_stopPolling() {
|
|
2622
2770
|
if (this._pollHandle !== null) {
|
|
2623
2771
|
window.clearInterval(this._pollHandle);
|
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;
|