jupyterlab_ai_code_assistants_extension 1.2.282 → 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 +23 -1
- package/lib/core/panel.d.ts +14 -2
- package/lib/core/panel.js +75 -14
- 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 +169 -0
- package/src/__tests__/request.spec.ts +25 -1
- package/src/core/panel.ts +81 -16
- 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/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,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
|
|
@@ -899,7 +923,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
899
923
|
// The kept listing names conversations that may have just gone.
|
|
900
924
|
this._branchCache = null;
|
|
901
925
|
bar.value = 1;
|
|
902
|
-
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(' ');
|
|
903
932
|
// Refresh so the row's conversation count and menu label update.
|
|
904
933
|
await this._fetch();
|
|
905
934
|
}
|
|
@@ -1502,6 +1531,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1502
1531
|
await this._colours.forget((_a = result.removed_ids) !== null && _a !== void 0 ? _a : sessionIds);
|
|
1503
1532
|
// The kept listing names conversations that may have just gone.
|
|
1504
1533
|
this._branchCache = null;
|
|
1534
|
+
const kept = keptCopy(result.live_ids);
|
|
1535
|
+
if (kept) {
|
|
1536
|
+
Notification.info(kept, { autoClose: 4000 });
|
|
1537
|
+
}
|
|
1505
1538
|
return result.removed_count;
|
|
1506
1539
|
}
|
|
1507
1540
|
catch (err) {
|
|
@@ -2420,6 +2453,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2420
2453
|
// callback still needs to read it. The field is overwritten on the next
|
|
2421
2454
|
// contextmenu open.
|
|
2422
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
|
+
}
|
|
2423
2461
|
});
|
|
2424
2462
|
}
|
|
2425
2463
|
_menu(commands = this._commands) {
|
|
@@ -2698,13 +2736,36 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2698
2736
|
if (this._polling !== null) {
|
|
2699
2737
|
return;
|
|
2700
2738
|
}
|
|
2701
|
-
this.
|
|
2702
|
-
.catch(err => this._showError(err))
|
|
2703
|
-
.finally(() => {
|
|
2704
|
-
this._polling = null;
|
|
2705
|
-
});
|
|
2739
|
+
this._poll();
|
|
2706
2740
|
}, POLL_INTERVAL_MS);
|
|
2707
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
|
+
}
|
|
2708
2769
|
_stopPolling() {
|
|
2709
2770
|
if (this._pollHandle !== null) {
|
|
2710
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;
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ILabShell, ILayoutRestorer } from '@jupyterlab/application';
|
|
2
2
|
import { ICommandPalette, Notification } from '@jupyterlab/apputils';
|
|
3
|
+
import { URLExt } from '@jupyterlab/coreutils';
|
|
3
4
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
4
5
|
import { ILauncher } from '@jupyterlab/launcher';
|
|
5
6
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
@@ -11,7 +12,7 @@ import { launcherTileIcon, providerIcon } from './core/icons';
|
|
|
11
12
|
import { DEFAULT_RECENT_LIMIT } from './core/limits';
|
|
12
13
|
import { AssistantSessionsPanel, DEFAULT_PRESENTATION_MODE, MOVE_PANELS_COMMAND, SYNC_FILE_BROWSER_COMMAND, commandId, panelWidgetId } from './core/panel';
|
|
13
14
|
import { LOG_PREFIX, ProviderRegistry } from './core/registry';
|
|
14
|
-
import { requestAPI } from './core/request';
|
|
15
|
+
import { API_NAMESPACE, requestAPI, WINDOW_ID } from './core/request';
|
|
15
16
|
import { PROVIDERS } from './providers';
|
|
16
17
|
/** Which sidebar the panels dock into unless the user says otherwise. */
|
|
17
18
|
const DEFAULT_SIDEBAR = 'right';
|
|
@@ -21,9 +22,10 @@ const PLUGIN_ID = 'jupyterlab_ai_code_assistants_extension:plugin';
|
|
|
21
22
|
const SHELL_PLUGIN_ID = '@jupyterlab/application-extension:shell';
|
|
22
23
|
/** Sidebar rank, shared by every panel so they dock together. */
|
|
23
24
|
const PANEL_RANK = 600;
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
const
|
|
25
|
+
/** The events socket reopens after a drop on a doubling delay, from the base
|
|
26
|
+
* up to the cap (the house notifications stream's values). */
|
|
27
|
+
const RECONNECT_BASE_MS = 5000;
|
|
28
|
+
const RECONNECT_MAX_MS = 60000;
|
|
27
29
|
/** Launcher section every assistant tile lands in. */
|
|
28
30
|
const LAUNCHER_CATEGORY = 'AI Assistants';
|
|
29
31
|
/** Where that section sits. The Launcher ranks Notebook 0, Console 20 and
|
|
@@ -108,8 +110,8 @@ const plugin = {
|
|
|
108
110
|
});
|
|
109
111
|
}
|
|
110
112
|
catch (err) {
|
|
111
|
-
// No cadence named: this also prints from the
|
|
112
|
-
//
|
|
113
|
+
// No cadence named: there is none, and this also prints from the
|
|
114
|
+
// activation probe, before the wake listeners are armed (DEF-122).
|
|
113
115
|
console.warn(`${LOG_PREFIX} status probe failed; the last known roster stands (every enabled assistant, when there is none yet) until a later probe answers.`, err);
|
|
114
116
|
}
|
|
115
117
|
};
|
|
@@ -189,7 +191,9 @@ const plugin = {
|
|
|
189
191
|
// survives a reload independently of the others.
|
|
190
192
|
restorer === null || restorer === void 0 ? void 0 : restorer.add(panel, panelWidgetId(id));
|
|
191
193
|
const command = app.commands.addCommand(commandId(id, 'refresh'), {
|
|
192
|
-
|
|
194
|
+
// The panel title is the assistant name alone (ACC-PANE-232); the
|
|
195
|
+
// command still names what it refreshes.
|
|
196
|
+
label: `Refresh ${module.descriptor.panelTitle} Sessions`,
|
|
193
197
|
// Takes no arguments, which is what `null` says here.
|
|
194
198
|
describedBy: { args: null },
|
|
195
199
|
execute: () => panel.refresh()
|
|
@@ -413,24 +417,49 @@ const plugin = {
|
|
|
413
417
|
}
|
|
414
418
|
}
|
|
415
419
|
};
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
420
|
+
// No timer checks which assistants are installed (ACC-PROV-230). The check
|
|
421
|
+
// runs at activation, when the tab becomes visible, when the network comes
|
|
422
|
+
// back and when the events socket reopens after a drop; a `refresh` push
|
|
423
|
+
// carries its roster and needs no request. Triggers that arrive before the
|
|
424
|
+
// request goes out - one macrotask - share it, and one request serves every
|
|
425
|
+
// panel of the window (ACC-PROV-231). A trigger while a request is in
|
|
426
|
+
// flight queues exactly one more after it: a wake is never answered by a
|
|
427
|
+
// request sent before it (DEF-117).
|
|
422
428
|
// Heard from here, before the first await: a wake before the listeners are
|
|
423
429
|
// armed below is only remembered, because a reconcile before the settings
|
|
424
430
|
// load reads the enabled-by-default fallback and docks assistants the user
|
|
425
431
|
// disabled; one status check answers it once they are armed (DEF-GUARD-122).
|
|
426
432
|
let armed = false;
|
|
427
433
|
let wokeDuringActivation = false;
|
|
434
|
+
/** A status request is queued or in flight. */
|
|
435
|
+
let probing = false;
|
|
436
|
+
/** A trigger arrived that the request in flight was sent before. */
|
|
437
|
+
let wokeAgain = false;
|
|
428
438
|
const reprobe = () => {
|
|
429
439
|
if (!armed) {
|
|
430
440
|
wokeDuringActivation = true;
|
|
431
441
|
return;
|
|
432
442
|
}
|
|
433
|
-
|
|
443
|
+
wokeAgain = true;
|
|
444
|
+
if (probing) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
probing = true;
|
|
448
|
+
const send = () => {
|
|
449
|
+
wokeAgain = false;
|
|
450
|
+
void probeStatus()
|
|
451
|
+
.then(reconcile)
|
|
452
|
+
.finally(() => {
|
|
453
|
+
if (wokeAgain) {
|
|
454
|
+
send();
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
probing = false;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
// Runs once per trigger burst, never on a cadence.
|
|
462
|
+
window.setTimeout(send, 0);
|
|
434
463
|
};
|
|
435
464
|
window.addEventListener('online', reprobe);
|
|
436
465
|
document.addEventListener('visibilitychange', () => {
|
|
@@ -510,11 +539,6 @@ const plugin = {
|
|
|
510
539
|
});
|
|
511
540
|
await migrate();
|
|
512
541
|
reconcile();
|
|
513
|
-
// Re-probe on a slow cadence so a binary installed (or removed) while
|
|
514
|
-
// JupyterLab runs takes effect on the next refresh, without a reload.
|
|
515
|
-
window.setInterval(() => {
|
|
516
|
-
void probeStatus().then(reconcile);
|
|
517
|
-
}, STATUS_INTERVAL_MS);
|
|
518
542
|
armed = true;
|
|
519
543
|
if (wokeDuringActivation) {
|
|
520
544
|
reprobe();
|
|
@@ -552,6 +576,72 @@ const plugin = {
|
|
|
552
576
|
channel.postMessage('changed');
|
|
553
577
|
});
|
|
554
578
|
}
|
|
579
|
+
// One events socket per window: the server pushes a message after every
|
|
580
|
+
// change a route makes, whoever asked for it - the command line included
|
|
581
|
+
// (ACC-CLI-227, ACC-CLI-228). Opened last, as the house notifications
|
|
582
|
+
// stream is, so nothing it throws can take the panels down with it.
|
|
583
|
+
let eventsUrl = URLExt.join(serverSettings.wsUrl, API_NAMESPACE, 'events');
|
|
584
|
+
// Gated on appendToken, as @jupyterlab/services does for its own sockets:
|
|
585
|
+
// a same-host session carries the handshake in its cookie.
|
|
586
|
+
if (serverSettings.appendToken && serverSettings.token) {
|
|
587
|
+
eventsUrl += `?token=${encodeURIComponent(serverSettings.token)}`;
|
|
588
|
+
}
|
|
589
|
+
let socket = null;
|
|
590
|
+
/** Closes since the last open; non-zero means pushes may have been missed. */
|
|
591
|
+
let drops = 0;
|
|
592
|
+
let reopenTimer;
|
|
593
|
+
const openEvents = () => {
|
|
594
|
+
window.clearTimeout(reopenTimer);
|
|
595
|
+
const ws = new serverSettings.WebSocket(eventsUrl);
|
|
596
|
+
socket = ws;
|
|
597
|
+
ws.onopen = () => {
|
|
598
|
+
if (drops > 0) {
|
|
599
|
+
// Whatever was pushed while the socket was closed: one status check
|
|
600
|
+
// and one read per panel (the network-back trigger of ACC-PROV-230).
|
|
601
|
+
drops = 0;
|
|
602
|
+
reprobe();
|
|
603
|
+
live.forEach(entry => entry.panel.sessionsChanged());
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
ws.onmessage = (event) => {
|
|
607
|
+
var _a, _b;
|
|
608
|
+
const message = JSON.parse(event.data);
|
|
609
|
+
if (message.type === 'sessions-changed') {
|
|
610
|
+
// This window's own change: the panel that made it re-reads after
|
|
611
|
+
// the write, and a second read redraws every row again
|
|
612
|
+
// (DEF-PANE-370-OWN-PUSH).
|
|
613
|
+
if (message.window !== WINDOW_ID) {
|
|
614
|
+
(_b = live.get((_a = message.provider) !== null && _a !== void 0 ? _a : '')) === null || _b === void 0 ? void 0 : _b.panel.sessionsChanged();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else if (message.type === 'refresh' && message.status) {
|
|
618
|
+
// The server probed for this push; its roster needs no request.
|
|
619
|
+
status = message.status;
|
|
620
|
+
reconcile();
|
|
621
|
+
live.forEach(entry => entry.panel.sessionsChanged());
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
ws.onclose = () => {
|
|
625
|
+
socket = null;
|
|
626
|
+
drops += 1;
|
|
627
|
+
// This timer only reopens the socket; it checks nothing.
|
|
628
|
+
reopenTimer = window.setTimeout(openEvents, Math.min(RECONNECT_BASE_MS * 2 ** (drops - 1), RECONNECT_MAX_MS));
|
|
629
|
+
};
|
|
630
|
+
ws.onerror = () => ws.close();
|
|
631
|
+
};
|
|
632
|
+
// A closed socket reopens at once when the tab is back or the network is.
|
|
633
|
+
const reopenEvents = () => {
|
|
634
|
+
if (socket === null) {
|
|
635
|
+
openEvents();
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
window.addEventListener('online', reopenEvents);
|
|
639
|
+
document.addEventListener('visibilitychange', () => {
|
|
640
|
+
if (document.visibilityState === 'visible') {
|
|
641
|
+
reopenEvents();
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
openEvents();
|
|
555
645
|
console.log(`${LOG_PREFIX} ${live.size} of ${registry.ids.length} assistant panel(s) docked.`);
|
|
556
646
|
}
|
|
557
647
|
};
|
package/lib/providers/claude.js
CHANGED
|
@@ -30,7 +30,7 @@ const claudeSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.433 -0
|
|
|
30
30
|
export const descriptor = {
|
|
31
31
|
id: 'claude',
|
|
32
32
|
label: 'Claude Code',
|
|
33
|
-
panelTitle: 'Claude Code
|
|
33
|
+
panelTitle: 'Claude Code',
|
|
34
34
|
iconName: 'jupyterlab_ai_code_assistants_extension:claude',
|
|
35
35
|
iconSvg: claudeSvgStr,
|
|
36
36
|
cliBinary: 'claude',
|
package/lib/providers/codex.js
CHANGED
|
@@ -28,7 +28,7 @@ const BYPASS_MODE = 'dangerouslyBypassApprovalsAndSandbox';
|
|
|
28
28
|
export const descriptor = {
|
|
29
29
|
id: 'codex',
|
|
30
30
|
label: 'Codex',
|
|
31
|
-
panelTitle: 'Codex
|
|
31
|
+
panelTitle: 'Codex',
|
|
32
32
|
iconName: 'jupyterlab_ai_code_assistants_extension:codex',
|
|
33
33
|
iconSvg: codexSvgStr,
|
|
34
34
|
cliBinary: 'codex',
|
|
@@ -27,7 +27,7 @@ const deepseekSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2.5 -4
|
|
|
27
27
|
export const descriptor = {
|
|
28
28
|
id: 'deepseek',
|
|
29
29
|
label: 'DeepSeek',
|
|
30
|
-
panelTitle: 'DeepSeek
|
|
30
|
+
panelTitle: 'DeepSeek',
|
|
31
31
|
iconName: 'jupyterlab_ai_code_assistants_extension:deepseek',
|
|
32
32
|
iconSvg: deepseekSvgStr,
|
|
33
33
|
cliBinary: 'dsh',
|
package/lib/providers/gemini.js
CHANGED
|
@@ -31,7 +31,7 @@ const YOLO_MODE = 'yoloMode';
|
|
|
31
31
|
export const descriptor = {
|
|
32
32
|
id: 'gemini',
|
|
33
33
|
label: 'Gemini',
|
|
34
|
-
panelTitle: 'Gemini
|
|
34
|
+
panelTitle: 'Gemini',
|
|
35
35
|
iconName: 'jupyterlab_ai_code_assistants_extension:gemini',
|
|
36
36
|
iconSvg: geminiSvgStr,
|
|
37
37
|
cliBinary: 'gemini',
|
package/lib/providers/kimi.js
CHANGED
|
@@ -28,7 +28,7 @@ const kimiSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2.395 -1.8
|
|
|
28
28
|
export const descriptor = {
|
|
29
29
|
id: 'kimi',
|
|
30
30
|
label: 'Kimi',
|
|
31
|
-
panelTitle: 'Kimi Code
|
|
31
|
+
panelTitle: 'Kimi Code',
|
|
32
32
|
iconName: 'jupyterlab_ai_code_assistants_extension:kimi',
|
|
33
33
|
iconSvg: kimiSvgStr,
|
|
34
34
|
cliBinary: 'kimi',
|
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.288",
|
|
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",
|
package/schema/plugin.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"providers.claude.enabled": {
|
|
43
43
|
"type": "boolean",
|
|
44
44
|
"title": "Claude Code: enabled",
|
|
45
|
-
"description": "Show the Claude Code
|
|
45
|
+
"description": "Show the Claude Code panel. A panel appears only when this is on AND the `claude` binary is on PATH.",
|
|
46
46
|
"default": true
|
|
47
47
|
},
|
|
48
48
|
"providers.claude.dangerouslySkipPermissions": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"providers.codex.enabled": {
|
|
55
55
|
"type": "boolean",
|
|
56
56
|
"title": "Codex: enabled",
|
|
57
|
-
"description": "Show the Codex
|
|
57
|
+
"description": "Show the Codex panel. A panel appears only when this is on AND the `codex` binary is on PATH.",
|
|
58
58
|
"default": true
|
|
59
59
|
},
|
|
60
60
|
"providers.codex.dangerouslyBypassApprovalsAndSandbox": {
|
|
@@ -66,13 +66,13 @@
|
|
|
66
66
|
"providers.deepseek.enabled": {
|
|
67
67
|
"type": "boolean",
|
|
68
68
|
"title": "DeepSeek: enabled",
|
|
69
|
-
"description": "Show the DeepSeek
|
|
69
|
+
"description": "Show the DeepSeek panel. A panel appears only when this is on AND the `dsh` binary is on PATH.",
|
|
70
70
|
"default": true
|
|
71
71
|
},
|
|
72
72
|
"providers.gemini.enabled": {
|
|
73
73
|
"type": "boolean",
|
|
74
74
|
"title": "Gemini: enabled",
|
|
75
|
-
"description": "Show the Gemini
|
|
75
|
+
"description": "Show the Gemini panel. A panel appears only when this is on AND the `gemini` binary is on PATH.",
|
|
76
76
|
"default": true
|
|
77
77
|
},
|
|
78
78
|
"providers.gemini.yoloMode": {
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"providers.kimi.enabled": {
|
|
85
85
|
"type": "boolean",
|
|
86
86
|
"title": "Kimi: enabled",
|
|
87
|
-
"description": "Show the Kimi Code
|
|
87
|
+
"description": "Show the Kimi Code panel. A panel appears only when this is on AND the `kimi` binary is on PATH.",
|
|
88
88
|
"default": true
|
|
89
89
|
},
|
|
90
90
|
"providers.kimi.yoloMode": {
|