jupyterlab_ai_code_assistants_extension 1.2.56 → 1.2.71
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 +1 -0
- package/lib/core/panel.d.ts +25 -0
- package/lib/core/panel.js +74 -2
- package/lib/core/request.d.ts +9 -9
- package/lib/core/request.js +9 -9
- package/lib/index.js +93 -1
- package/package.json +1 -1
- package/schema/plugin.json +6 -0
- package/src/__tests__/index.spec.ts +630 -14
- package/src/__tests__/panel.spec.ts +482 -1
- package/src/core/panel.ts +85 -2
- package/src/core/request.ts +9 -9
- package/src/index.ts +118 -0
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
30
30
|
- **Joint settings page** - one settings section covering all assistants, with a per-assistant toggle (all on by default); toggling takes effect live, no JupyterLab reload, and a change reaches the other JupyterLab windows of the same browser
|
|
31
31
|
- **Three-section side panel** - Favorites, Recent, and All projects, each scrolling independently
|
|
32
32
|
- **One-click resume** - click a row to jump back into that session in a terminal; an open terminal for the project is reused instead of duplicated. Exception: a Codex or Kimi conversation started with `+`, and a Codex fork, run without an id until the CLI writes one, so a click on that row meanwhile opens a second terminal
|
|
33
|
+
- **File browser follows the opened session** - opening a session moves the JupyterLab file browser to that session's 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
|
|
33
34
|
- **Launcher tiles** - every enabled assistant has a tile in an "AI Assistants" section of the JupyterLab Launcher (after Other, under the extension's own robot-head icon); a click opens the assistant in the file browser's current folder, resuming the folder's conversation when one exists and starting a new one otherwise, through the companion `jupyterlab_basic_terminal_extension` (installed automatically). Disable an assistant and its tile goes; disable them all and the section goes with them
|
|
34
35
|
- **Your terminal's `claude -c` follows the panel** (Claude) - switching to a conversation makes plain `claude -c` in that project resume that same conversation, including one that has been compacted, which Claude Code otherwise refuses to continue. Launching repairs a compacted conversation the same way, but only switching moves what `claude -c` picks
|
|
35
36
|
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id with last-activity time; "Manage Sessions..." opens a searchable popup over the full list with multi-select delete and per-row open and copy-id buttons
|
package/lib/core/panel.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export declare function panelWidgetId(providerId: string): string;
|
|
|
13
13
|
/** Command id for one provider's action. Namespaced per provider, so two
|
|
14
14
|
* panels never collide in a shared registry. */
|
|
15
15
|
export declare function commandId(providerId: string, action: string): string;
|
|
16
|
+
/** Command ids of the two items in a panel header's context menu. The plugin
|
|
17
|
+
* registers them once, on the application registry, because the settings they
|
|
18
|
+
* write are shared by every panel. */
|
|
19
|
+
export declare const SYNC_FILE_BROWSER_COMMAND = "ai-code-assistants:toggle-file-browser-sync";
|
|
20
|
+
export declare const MOVE_PANELS_COMMAND = "ai-code-assistants:move-panels";
|
|
16
21
|
/**
|
|
17
22
|
* One assistant's sessions panel.
|
|
18
23
|
*
|
|
@@ -34,6 +39,8 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
34
39
|
* key is an `ILaunchMode.id` in the assistant's own terminology. */
|
|
35
40
|
setModes(modes: Record<string, boolean | string>): void;
|
|
36
41
|
setColouredTabs(on: boolean): void;
|
|
42
|
+
/** Whether opening a session moves the file browser to its project folder. */
|
|
43
|
+
setSyncFileBrowser(on: boolean): void;
|
|
37
44
|
protected onAfterShow(_msg: Message): void;
|
|
38
45
|
protected onBeforeHide(_msg: Message): void;
|
|
39
46
|
protected onCloseRequest(msg: Message): void;
|
|
@@ -90,6 +97,16 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
90
97
|
* `resolveLaunchMode` for the precedence rule. */
|
|
91
98
|
private _launchMode;
|
|
92
99
|
private _openSession;
|
|
100
|
+
/** Move the file browser to an opened session's project folder, when the
|
|
101
|
+
* setting is on and the folder is under the server root. A folder the file
|
|
102
|
+
* browser cannot address is left alone without a notification.
|
|
103
|
+
*
|
|
104
|
+
* `dontShowBrowser` stops JupyterLab activating the file browser tab, which
|
|
105
|
+
* would hide an assistant panel docked in the same sidebar. A failed move
|
|
106
|
+
* is one console line and never a failed open: JupyterLab's command logs a
|
|
107
|
+
* folder it cannot reach, and the catch below logs a command that is
|
|
108
|
+
* missing or rejects. */
|
|
109
|
+
private _moveFileBrowserTo;
|
|
93
110
|
/** The server root every path is shown and resolved against, plus
|
|
94
111
|
* JupyterLab's move-to-trash setting. Settable because a panel docked before
|
|
95
112
|
* the server has answered its first status probe is built without either
|
|
@@ -269,6 +286,12 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
269
286
|
private _visibleVariantCount;
|
|
270
287
|
private _buildMenus;
|
|
271
288
|
private _menu;
|
|
289
|
+
/** Open the header's context menu at the pointer. Its two commands are on
|
|
290
|
+
* the application registry, where the plugin registers them once for every
|
|
291
|
+
* panel. The menu is built on its first open and not with the menus above:
|
|
292
|
+
* Lumino draws the items of a menu once they are added, and these two are
|
|
293
|
+
* drawn from that registry. */
|
|
294
|
+
private _openHeaderMenu;
|
|
272
295
|
/** Rebuild the context menu's items. Lumino submenu items have no
|
|
273
296
|
* `isVisible` hook, so the menu is rebuilt per open and a branch submenu is
|
|
274
297
|
* inserted only when the row actually has branches - a capability the
|
|
@@ -330,11 +353,13 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
330
353
|
private _openBranchSubmenu;
|
|
331
354
|
private _branchSessionMenu;
|
|
332
355
|
private _newSessionMenu;
|
|
356
|
+
private _headerMenu;
|
|
333
357
|
/** Re-writes the `+` button's title. Set once the shell exists. */
|
|
334
358
|
private _renameNewButton;
|
|
335
359
|
private _lastBranches;
|
|
336
360
|
private _lastBranchesCurrent;
|
|
337
361
|
private _colouredTabs;
|
|
362
|
+
private _syncFileBrowser;
|
|
338
363
|
private _resettableColours;
|
|
339
364
|
/** Bumped per context-menu open, so a slow one that has been superseded
|
|
340
365
|
* knows to touch nothing. */
|
package/lib/core/panel.js
CHANGED
|
@@ -63,6 +63,17 @@ export function panelWidgetId(providerId) {
|
|
|
63
63
|
export function commandId(providerId, action) {
|
|
64
64
|
return `ai-code-assistants:${providerId}:${action}`;
|
|
65
65
|
}
|
|
66
|
+
/** Command ids of the two items in a panel header's context menu. The plugin
|
|
67
|
+
* registers them once, on the application registry, because the settings they
|
|
68
|
+
* write are shared by every panel. */
|
|
69
|
+
export const SYNC_FILE_BROWSER_COMMAND = 'ai-code-assistants:toggle-file-browser-sync';
|
|
70
|
+
export const MOVE_PANELS_COMMAND = 'ai-code-assistants:move-panels';
|
|
71
|
+
/** The file browser moves of every panel, one after another in the order
|
|
72
|
+
* their opens finished. JupyterLab's command asks the server about a folder
|
|
73
|
+
* before it changes to it, so two moves that overlapped were applied in the
|
|
74
|
+
* order the answers arrived, and the file browser could end on the session
|
|
75
|
+
* opened first. */
|
|
76
|
+
let fileBrowserMoves = Promise.resolve();
|
|
66
77
|
function loadExpanded(storageKey) {
|
|
67
78
|
try {
|
|
68
79
|
const raw = window.localStorage.getItem(storageKey);
|
|
@@ -114,11 +125,13 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
114
125
|
this._sessions = null;
|
|
115
126
|
/** Which row carries each section's single tab stop, by encoded path. */
|
|
116
127
|
this._roving = {};
|
|
128
|
+
this._headerMenu = null;
|
|
117
129
|
/** Re-writes the `+` button's title. Set once the shell exists. */
|
|
118
130
|
this._renameNewButton = null;
|
|
119
131
|
this._lastBranches = [];
|
|
120
132
|
this._lastBranchesCurrent = '';
|
|
121
133
|
this._colouredTabs = true;
|
|
134
|
+
this._syncFileBrowser = true;
|
|
122
135
|
this._resettableColours = [];
|
|
123
136
|
/** Bumped per context-menu open, so a slow one that has been superseded
|
|
124
137
|
* knows to touch nothing. */
|
|
@@ -235,6 +248,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
235
248
|
this._colouredTabs = on;
|
|
236
249
|
this._terminals.setColouredTabs(on);
|
|
237
250
|
}
|
|
251
|
+
/** Whether opening a session moves the file browser to its project folder. */
|
|
252
|
+
setSyncFileBrowser(on) {
|
|
253
|
+
this._syncFileBrowser = on;
|
|
254
|
+
}
|
|
238
255
|
onAfterShow(_msg) {
|
|
239
256
|
this.refresh();
|
|
240
257
|
this._startPolling();
|
|
@@ -256,6 +273,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
256
273
|
title.className = 'jp-AiAssistantsPanel-title';
|
|
257
274
|
title.textContent = this._descriptor.panelTitle;
|
|
258
275
|
header.appendChild(title);
|
|
276
|
+
// The header has a context menu of its own, for the settings every panel
|
|
277
|
+
// shares. It replaces the browser's menu here.
|
|
278
|
+
header.addEventListener('contextmenu', e => {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
this._openHeaderMenu(e.clientX, e.clientY);
|
|
281
|
+
});
|
|
259
282
|
const newBtn = document.createElement('button');
|
|
260
283
|
newBtn.className = 'jp-AiAssistantsPanel-iconButton';
|
|
261
284
|
// The title is a static string - the folder is deliberately NOT named
|
|
@@ -729,6 +752,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
729
752
|
async _openSession(session, force, sessionId) {
|
|
730
753
|
try {
|
|
731
754
|
await this._terminals.openSession(session, this._launchMode(force), sessionId);
|
|
755
|
+
// Not awaited: the open has succeeded, and the sync is not part of it.
|
|
756
|
+
fileBrowserMoves = fileBrowserMoves.then(() => this._moveFileBrowserTo(session));
|
|
732
757
|
}
|
|
733
758
|
catch (err) {
|
|
734
759
|
if (isResponseStatus(err, 404)) {
|
|
@@ -744,6 +769,33 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
744
769
|
void this._fetch().catch(() => undefined);
|
|
745
770
|
}
|
|
746
771
|
}
|
|
772
|
+
/** Move the file browser to an opened session's project folder, when the
|
|
773
|
+
* setting is on and the folder is under the server root. A folder the file
|
|
774
|
+
* browser cannot address is left alone without a notification.
|
|
775
|
+
*
|
|
776
|
+
* `dontShowBrowser` stops JupyterLab activating the file browser tab, which
|
|
777
|
+
* would hide an assistant panel docked in the same sidebar. A failed move
|
|
778
|
+
* is one console line and never a failed open: JupyterLab's command logs a
|
|
779
|
+
* folder it cannot reach, and the catch below logs a command that is
|
|
780
|
+
* missing or rejects. */
|
|
781
|
+
async _moveFileBrowserTo(session) {
|
|
782
|
+
if (!this._syncFileBrowser) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const rel = this._pathUnderRoot(session.project_path);
|
|
786
|
+
if (rel === null) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
await this._app.commands.execute('filebrowser:go-to-path', {
|
|
791
|
+
path: rel,
|
|
792
|
+
dontShowBrowser: true
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
catch (err) {
|
|
796
|
+
this._logError(`The file browser did not move to the project folder: ${err instanceof Error ? err.message : String(err)}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
747
799
|
/** The server root every path is shown and resolved against, plus
|
|
748
800
|
* JupyterLab's move-to-trash setting. Settable because a panel docked before
|
|
749
801
|
* the server has answered its first status probe is built without either
|
|
@@ -2219,7 +2271,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2219
2271
|
this._setActiveRow(null);
|
|
2220
2272
|
});
|
|
2221
2273
|
}
|
|
2222
|
-
_menu() {
|
|
2274
|
+
_menu(commands = this._commands) {
|
|
2223
2275
|
// MenuSvg, not a plain Lumino Menu. Lumino's default renderer emits
|
|
2224
2276
|
// `div.lm-Menu-itemSubmenuIcon` EMPTY, so every submenu here drew no caret
|
|
2225
2277
|
// and `Branch Session`, which opens on hover, read exactly like the items
|
|
@@ -2227,10 +2279,30 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2227
2279
|
// own menus use: it fills that div with the caret glyph, and otherwise
|
|
2228
2280
|
// differs only in adding the stock menu-item icon sizing our items already
|
|
2229
2281
|
// get from `base.css`.
|
|
2230
|
-
const menu = new MenuSvg({ commands
|
|
2282
|
+
const menu = new MenuSvg({ commands });
|
|
2231
2283
|
menu.addClass('jp-AiAssistantsContextMenu');
|
|
2232
2284
|
return menu;
|
|
2233
2285
|
}
|
|
2286
|
+
/** Open the header's context menu at the pointer. Its two commands are on
|
|
2287
|
+
* the application registry, where the plugin registers them once for every
|
|
2288
|
+
* panel. The menu is built on its first open and not with the menus above:
|
|
2289
|
+
* Lumino draws the items of a menu once they are added, and these two are
|
|
2290
|
+
* drawn from that registry. */
|
|
2291
|
+
_openHeaderMenu(x, y) {
|
|
2292
|
+
// A row menu that is still loading stands down, as it does for a newer
|
|
2293
|
+
// row menu, and its row loses the highlight.
|
|
2294
|
+
this._menuGeneration += 1;
|
|
2295
|
+
this._setActiveRow(null);
|
|
2296
|
+
if (!this._headerMenu) {
|
|
2297
|
+
this._headerMenu = this._menu(this._app.commands);
|
|
2298
|
+
this._headerMenu.addItem({ command: SYNC_FILE_BROWSER_COMMAND });
|
|
2299
|
+
this._headerMenu.addItem({
|
|
2300
|
+
command: MOVE_PANELS_COMMAND,
|
|
2301
|
+
args: { panel: this.id }
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
this._headerMenu.open(x, y);
|
|
2305
|
+
}
|
|
2234
2306
|
/** Rebuild the context menu's items. Lumino submenu items have no
|
|
2235
2307
|
* `isVisible` hook, so the menu is rebuilt per open and a branch submenu is
|
|
2236
2308
|
* inserted only when the row actually has branches - a capability the
|
package/lib/core/request.d.ts
CHANGED
|
@@ -4,21 +4,21 @@ export declare const API_NAMESPACE = "jupyterlab-ai-code-assistants-extension";
|
|
|
4
4
|
/**
|
|
5
5
|
* DEF-72: how long a single server call may run before the client gives up.
|
|
6
6
|
*
|
|
7
|
-
* The number is not free to pick.
|
|
8
|
-
*
|
|
9
|
-
* disposal or fork can legitimately hold the server for
|
|
10
|
-
* client ceiling at or below that would cut
|
|
11
|
-
* path the server number was tuned for, so this
|
|
12
|
-
* it. Do not tune it down to the fast path.
|
|
7
|
+
* The number is not free to pick. The provider module that defines
|
|
8
|
+
* `_CLI_TIMEOUT_S = 30` caps ONE archive or delete CLI invocation at it, so a
|
|
9
|
+
* disposal or fork of that provider can legitimately hold the server for
|
|
10
|
+
* thirty seconds. A client ceiling at or below that would cut that provider
|
|
11
|
+
* silently on exactly the slow path the server number was tuned for, so this
|
|
12
|
+
* sits a full CLI timeout above it. Do not tune it down to the fast path.
|
|
13
13
|
*
|
|
14
14
|
* It bounds SINGLE-shot routes. Two server paths scale with N and can exceed
|
|
15
15
|
* it honestly: a project DELETE runs one CLI invocation per conversation
|
|
16
|
-
* (`
|
|
17
|
-
* sessions listing runs one `git` per project root capped at
|
|
16
|
+
* (that provider's `_dispose`, ~0.7s each measured, 30s each worst case), and
|
|
17
|
+
* a sessions listing runs one `git` per project root capped at
|
|
18
18
|
* `GIT_BRANCH_TIMEOUT_S = 2`. The `signal` hatch waives the ceiling for such a
|
|
19
19
|
* caller, but no caller in `src/` passes one today - only `request.spec.ts`
|
|
20
20
|
* exercises it. Measured, a project DELETE has to hold 86 conversations in one
|
|
21
|
-
* project to breach at ~0.7s per
|
|
21
|
+
* project to breach at ~0.7s per CLI invocation, or 858 at ~70ms, and a
|
|
22
22
|
* breach costs a spurious toast rather than data: the server thread runs to
|
|
23
23
|
* completion and the retry is idempotent.
|
|
24
24
|
*/
|
package/lib/core/request.js
CHANGED
|
@@ -6,21 +6,21 @@ export const API_NAMESPACE = 'jupyterlab-ai-code-assistants-extension';
|
|
|
6
6
|
/**
|
|
7
7
|
* DEF-72: how long a single server call may run before the client gives up.
|
|
8
8
|
*
|
|
9
|
-
* The number is not free to pick.
|
|
10
|
-
*
|
|
11
|
-
* disposal or fork can legitimately hold the server for
|
|
12
|
-
* client ceiling at or below that would cut
|
|
13
|
-
* path the server number was tuned for, so this
|
|
14
|
-
* it. Do not tune it down to the fast path.
|
|
9
|
+
* The number is not free to pick. The provider module that defines
|
|
10
|
+
* `_CLI_TIMEOUT_S = 30` caps ONE archive or delete CLI invocation at it, so a
|
|
11
|
+
* disposal or fork of that provider can legitimately hold the server for
|
|
12
|
+
* thirty seconds. A client ceiling at or below that would cut that provider
|
|
13
|
+
* silently on exactly the slow path the server number was tuned for, so this
|
|
14
|
+
* sits a full CLI timeout above it. Do not tune it down to the fast path.
|
|
15
15
|
*
|
|
16
16
|
* It bounds SINGLE-shot routes. Two server paths scale with N and can exceed
|
|
17
17
|
* it honestly: a project DELETE runs one CLI invocation per conversation
|
|
18
|
-
* (`
|
|
19
|
-
* sessions listing runs one `git` per project root capped at
|
|
18
|
+
* (that provider's `_dispose`, ~0.7s each measured, 30s each worst case), and
|
|
19
|
+
* a sessions listing runs one `git` per project root capped at
|
|
20
20
|
* `GIT_BRANCH_TIMEOUT_S = 2`. The `signal` hatch waives the ceiling for such a
|
|
21
21
|
* caller, but no caller in `src/` passes one today - only `request.spec.ts`
|
|
22
22
|
* exercises it. Measured, a project DELETE has to hold 86 conversations in one
|
|
23
|
-
* project to breach at ~0.7s per
|
|
23
|
+
* project to breach at ~0.7s per CLI invocation, or 858 at ~70ms, and a
|
|
24
24
|
* breach costs a spurious toast rather than data: the server thread runs to
|
|
25
25
|
* completion and the retry is idempotent.
|
|
26
26
|
*/
|
package/lib/index.js
CHANGED
|
@@ -9,13 +9,16 @@ import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
|
9
9
|
import { addCallbackCommand } from './core/callback';
|
|
10
10
|
import { launcherTileIcon, providerIcon } from './core/icons';
|
|
11
11
|
import { DEFAULT_RECENT_LIMIT } from './core/limits';
|
|
12
|
-
import { AssistantSessionsPanel, DEFAULT_PRESENTATION_MODE, commandId, panelWidgetId } from './core/panel';
|
|
12
|
+
import { AssistantSessionsPanel, DEFAULT_PRESENTATION_MODE, MOVE_PANELS_COMMAND, SYNC_FILE_BROWSER_COMMAND, commandId, panelWidgetId } from './core/panel';
|
|
13
13
|
import { LOG_PREFIX, ProviderRegistry } from './core/registry';
|
|
14
14
|
import { requestAPI } from './core/request';
|
|
15
15
|
import { PROVIDERS } from './providers';
|
|
16
16
|
/** Which sidebar the panels dock into unless the user says otherwise. */
|
|
17
17
|
const DEFAULT_SIDEBAR = 'right';
|
|
18
18
|
const PLUGIN_ID = 'jupyterlab_ai_code_assistants_extension:plugin';
|
|
19
|
+
/** JupyterLab's own plugin, whose `layout` setting stores the sidebar a user
|
|
20
|
+
* chose for a widget with "Switch Sidebar Side". */
|
|
21
|
+
const SHELL_PLUGIN_ID = '@jupyterlab/application-extension:shell';
|
|
19
22
|
/** Sidebar rank, shared by every panel so they dock together. */
|
|
20
23
|
const PANEL_RANK = 600;
|
|
21
24
|
/** How often the server is re-asked which assistants are available, so a CLI
|
|
@@ -84,6 +87,9 @@ const plugin = {
|
|
|
84
87
|
let status = null;
|
|
85
88
|
let settings = null;
|
|
86
89
|
let sidebar = DEFAULT_SIDEBAR;
|
|
90
|
+
/** The id of the panel whose header menu last asked for a move, until a
|
|
91
|
+
* move is applied. */
|
|
92
|
+
let moveAskedBy = null;
|
|
87
93
|
/** Ask the server which providers exist and which of their binaries are on
|
|
88
94
|
* PATH.
|
|
89
95
|
*
|
|
@@ -147,6 +153,8 @@ const plugin = {
|
|
|
147
153
|
}
|
|
148
154
|
// Defaults ON, so only an explicit `false` turns it off.
|
|
149
155
|
panel.setColouredTabs(setting('colouredTabs', true) !== false);
|
|
156
|
+
// Defaults ON as well: an absent key reads as on.
|
|
157
|
+
panel.setSyncFileBrowser(setting('syncFileBrowser', true) !== false);
|
|
150
158
|
panel.setModes(modesFor(panel.descriptor.id));
|
|
151
159
|
};
|
|
152
160
|
const dock = (panel) => {
|
|
@@ -358,10 +366,43 @@ const plugin = {
|
|
|
358
366
|
}
|
|
359
367
|
};
|
|
360
368
|
const applySidebar = () => {
|
|
369
|
+
var _a;
|
|
361
370
|
const next = setting('sidebar', DEFAULT_SIDEBAR);
|
|
362
371
|
if ((next === 'left' || next === 'right') && next !== sidebar) {
|
|
363
372
|
sidebar = next;
|
|
373
|
+
// Re-docking hides a panel, so a panel on screen before the move is
|
|
374
|
+
// activated again after it. Two can be on screen, one in each
|
|
375
|
+
// sidebar: the one whose header menu asked for the move is the one
|
|
376
|
+
// activated, else the first of them in `live`.
|
|
377
|
+
const onScreen = Array.from(live.values()).filter(entry => entry.panel.isVisible);
|
|
378
|
+
const shown = (_a = onScreen.find(entry => entry.panel.id === moveAskedBy)) !== null && _a !== void 0 ? _a : onScreen[0];
|
|
379
|
+
moveAskedBy = null;
|
|
364
380
|
live.forEach(entry => dock(entry.panel));
|
|
381
|
+
// A panel the user placed with JupyterLab's own "Switch Sidebar Side"
|
|
382
|
+
// carries a placement that `add` obeys, so it is still in the other
|
|
383
|
+
// sidebar. This change is the newer decision: it replaces that
|
|
384
|
+
// placement, the way JupyterLab's own command does, and JupyterLab's
|
|
385
|
+
// layout setting is saved so that the new side holds after a reload.
|
|
386
|
+
const elsewhere = new Set(labShell.widgets(sidebar === 'left' ? 'right' : 'left'));
|
|
387
|
+
let layout = null;
|
|
388
|
+
for (const entry of live.values()) {
|
|
389
|
+
if (elsewhere.has(entry.panel)) {
|
|
390
|
+
layout = labShell.move(entry.panel, sidebar);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (layout && settingRegistry) {
|
|
394
|
+
settingRegistry
|
|
395
|
+
.set(SHELL_PLUGIN_ID, 'layout', JSON.parse(JSON.stringify({
|
|
396
|
+
single: layout['single-document'],
|
|
397
|
+
multiple: layout['multiple-document']
|
|
398
|
+
})))
|
|
399
|
+
.catch(err => {
|
|
400
|
+
console.warn(`${LOG_PREFIX} could not save the panel placement.`, err);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (shown) {
|
|
404
|
+
labShell.activateById(shown.panel.id);
|
|
405
|
+
}
|
|
365
406
|
}
|
|
366
407
|
};
|
|
367
408
|
await probeStatus();
|
|
@@ -383,6 +424,57 @@ const plugin = {
|
|
|
383
424
|
settings = null;
|
|
384
425
|
}
|
|
385
426
|
}
|
|
427
|
+
/** Save one shared setting from a panel header's context menu. The
|
|
428
|
+
* `changed` handler above applies a saved value. A failed save does not
|
|
429
|
+
* change the composite value the menu reads, so the menu keeps showing
|
|
430
|
+
* the previous state. */
|
|
431
|
+
const saveSetting = async (key, value) => {
|
|
432
|
+
try {
|
|
433
|
+
await (settings === null || settings === void 0 ? void 0 : settings.set(key, value));
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
console.warn(`${LOG_PREFIX} could not save setting "${key}".`, err);
|
|
437
|
+
Notification.error(trans.__('The setting was not saved. Try again.'), {
|
|
438
|
+
autoClose: 4000
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
// The two items of every panel header's context menu. Registered once,
|
|
443
|
+
// here, because the settings they write are shared by every panel, and
|
|
444
|
+
// before the first panel exists.
|
|
445
|
+
app.commands.addCommand(SYNC_FILE_BROWSER_COMMAND, {
|
|
446
|
+
label: trans.__('Sync File Browser'),
|
|
447
|
+
// Takes no arguments, which is what `null` says here.
|
|
448
|
+
describedBy: { args: null },
|
|
449
|
+
isEnabled: () => settings !== null,
|
|
450
|
+
isToggled: () => setting('syncFileBrowser', true) !== false,
|
|
451
|
+
execute: () => saveSetting('syncFileBrowser', setting('syncFileBrowser', true) === false)
|
|
452
|
+
});
|
|
453
|
+
app.commands.addCommand(MOVE_PANELS_COMMAND, {
|
|
454
|
+
// Names the sidebar the panels are not in.
|
|
455
|
+
label: () => sidebar === 'right'
|
|
456
|
+
? trans.__('Move Panel Left')
|
|
457
|
+
: trans.__('Move Panel Right'),
|
|
458
|
+
describedBy: {
|
|
459
|
+
args: {
|
|
460
|
+
type: 'object',
|
|
461
|
+
properties: {
|
|
462
|
+
panel: {
|
|
463
|
+
type: 'string',
|
|
464
|
+
description: 'Id of the panel whose header menu ran the command.'
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
isEnabled: () => settings !== null,
|
|
470
|
+
execute: args => {
|
|
471
|
+
// `applySidebar` reads this when the change arrives. After a save
|
|
472
|
+
// that fails it stays set: the sidebar can then only change to the
|
|
473
|
+
// side this menu asked for.
|
|
474
|
+
moveAskedBy = typeof args.panel === 'string' ? args.panel : null;
|
|
475
|
+
return saveSetting('sidebar', sidebar === 'right' ? 'left' : 'right');
|
|
476
|
+
}
|
|
477
|
+
});
|
|
386
478
|
await migrate();
|
|
387
479
|
reconcile();
|
|
388
480
|
// Re-probe on a slow cadence so a binary installed (or removed) while
|
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.71",
|
|
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
|
@@ -33,6 +33,12 @@
|
|
|
33
33
|
"enum": ["left", "right"],
|
|
34
34
|
"default": "right"
|
|
35
35
|
},
|
|
36
|
+
"syncFileBrowser": {
|
|
37
|
+
"type": "boolean",
|
|
38
|
+
"title": "Sync file browser with opened session",
|
|
39
|
+
"description": "When enabled (default), opening a session moves the JupyterLab file browser to that session's project folder. A project folder outside the JupyterLab root leaves the file browser where it is. The context menu of each panel header also toggles this.",
|
|
40
|
+
"default": true
|
|
41
|
+
},
|
|
36
42
|
"providers.claude.enabled": {
|
|
37
43
|
"type": "boolean",
|
|
38
44
|
"title": "Claude Code: enabled",
|