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 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
@@ -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: this._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
@@ -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. `providers/codex.py` caps ONE `codex
8
- * archive` / `codex delete` invocation at `_CLI_TIMEOUT_S = 30`, so a Codex
9
- * disposal or fork can legitimately hold the server for thirty seconds. A
10
- * client ceiling at or below that would cut Codex silently on exactly the slow
11
- * path the server number was tuned for, so this sits a full CLI timeout above
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
- * (`CodexStore._dispose`, ~0.7s each measured, 30s each worst case), and a
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 Codex CLI invocation, or 858 at ~70ms, and a
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
  */
@@ -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. `providers/codex.py` caps ONE `codex
10
- * archive` / `codex delete` invocation at `_CLI_TIMEOUT_S = 30`, so a Codex
11
- * disposal or fork can legitimately hold the server for thirty seconds. A
12
- * client ceiling at or below that would cut Codex silently on exactly the slow
13
- * path the server number was tuned for, so this sits a full CLI timeout above
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
- * (`CodexStore._dispose`, ~0.7s each measured, 30s each worst case), and a
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 Codex CLI invocation, or 858 at ~70ms, and a
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.56",
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",
@@ -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",