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 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
- ![Claude Code Sessions panel](.resources/screenshot.png)
13
+ ![Claude Code panel](.resources/screenshot.png)
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.
@@ -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 reads it - an explicit
379
- * Refresh must always issue, however long the last poll is taking. */
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 reads it - an explicit
177
- * Refresh must always issue, however long the last poll is taking. */
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
- async _fetch() {
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
- this._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; });
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
- this._terminals.setSessions(this._sessions);
653
- this._render();
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
- void this._fetch().catch(() => undefined);
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 = `Removed ${data.removed_count} parallel session${data.removed_count === 1 ? '' : 's'}.`;
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._polling = this._fetch()
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);
@@ -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
  *
@@ -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
@@ -163,8 +163,12 @@ export interface IDisposalReport {
163
163
  removed_ids: string[];
164
164
  }
165
165
  export type IRemoveResponse = IDisposalReport;
166
- export type ICleanupResponse = IDisposalReport;
167
- export type IDeleteSessionsResponse = IDisposalReport;
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
- /** How often the server is re-asked which assistants are available, so a CLI
25
- * installed while JupyterLab runs surfaces without a reload. */
26
- const STATUS_INTERVAL_MS = 60000;
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 activation probe, before
112
- // the interval and the wake listeners are armed (DEF-122).
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
- label: `Refresh ${module.descriptor.panelTitle}`,
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
- // A suspended network stack - laptop asleep, tab backgrounded - is exactly
417
- // when the probe fails, and the slow cadence below would leave the roster
418
- // stale for up to a minute after the machine comes back. A wake fires
419
- // both events, so it costs two probes; deliberately, because a guard that
420
- // let the second join the first answered the wake with a request issued
421
- // before it, and starved the interval below of its own tick (DEF-117).
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
- void probeStatus().then(reconcile);
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
  };
@@ -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 Sessions',
33
+ panelTitle: 'Claude Code',
34
34
  iconName: 'jupyterlab_ai_code_assistants_extension:claude',
35
35
  iconSvg: claudeSvgStr,
36
36
  cliBinary: 'claude',
@@ -28,7 +28,7 @@ const BYPASS_MODE = 'dangerouslyBypassApprovalsAndSandbox';
28
28
  export const descriptor = {
29
29
  id: 'codex',
30
30
  label: 'Codex',
31
- panelTitle: 'Codex Sessions',
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 Sessions',
30
+ panelTitle: 'DeepSeek',
31
31
  iconName: 'jupyterlab_ai_code_assistants_extension:deepseek',
32
32
  iconSvg: deepseekSvgStr,
33
33
  cliBinary: 'dsh',
@@ -31,7 +31,7 @@ const YOLO_MODE = 'yoloMode';
31
31
  export const descriptor = {
32
32
  id: 'gemini',
33
33
  label: 'Gemini',
34
- panelTitle: 'Gemini Sessions',
34
+ panelTitle: 'Gemini',
35
35
  iconName: 'jupyterlab_ai_code_assistants_extension:gemini',
36
36
  iconSvg: geminiSvgStr,
37
37
  cliBinary: 'gemini',
@@ -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 Sessions',
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.282",
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",
@@ -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 Sessions panel. A panel appears only when this is on AND the `claude` binary is on PATH.",
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 Sessions panel. A panel appears only when this is on AND the `codex` binary is on PATH.",
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 Sessions panel. A panel appears only when this is on AND the `dsh` binary is on PATH.",
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 Sessions panel. A panel appears only when this is on AND the `gemini` binary is on PATH.",
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 Sessions panel. A panel appears only when this is on AND the `kimi` binary is on PATH.",
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": {