jupyterlab_ai_code_assistants_extension 1.2.99 → 1.2.115

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
@@ -76,6 +76,23 @@ pip install jupyterlab_ai_code_assistants_extension
76
76
  > [!WARNING]
77
77
  > `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
78
 
79
+ ## Agent skill
80
+
81
+ 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.
82
+
83
+ After `pip install`, with the Python that runs the lab:
84
+
85
+ ```bash
86
+ mkdir -p ~/.agents/skills
87
+ ln -s "$(python -c 'import sys; print(sys.prefix)')/share/jupyter/agents/skills/jupyterlab-ai-code-assistants-extension" ~/.agents/skills/jupyterlab-ai-code-assistants-extension
88
+ ```
89
+
90
+ From a clone:
91
+
92
+ ```bash
93
+ ln -s "$PWD/.agents/skills/jupyterlab-ai-code-assistants-extension" ~/.claude/skills/jupyterlab-ai-code-assistants-extension
94
+ ```
95
+
79
96
  ## Migrating from the standalone extensions
80
97
 
81
98
  This package supersedes `jupyterlab_claude_code_extension`, `jupyterlab_codex_extension` and `jupyterlab_kimi_code_extension`. On first run it carries their settings and favourites over automatically, and it never replaces a setting you have already saved in this extension; uninstall the standalones once you are ready:
@@ -45,10 +45,14 @@ export declare class AssistantSessionsPanel extends Widget {
45
45
  protected onBeforeHide(_msg: Message): void;
46
46
  protected onCloseRequest(msg: Message): void;
47
47
  private _buildShell;
48
- /** Raise the click-time busy overlay. Counted, because an open and a branch
49
- * can overlap; the overlay clears when the last one ends. */
48
+ /** Raise the click-time busy overlay. Each action gets a token, because an
49
+ * open and a branch can overlap; the overlay clears when the last one ends,
50
+ * and the label names the newest action still running. */
50
51
  private _beginBusy;
51
52
  private _endBusy;
53
+ /** Name the newest still-running action in the visible label and the live
54
+ * region, so neither keeps naming an action that ended. */
55
+ private _paintBusyMessage;
52
56
  /** Show / hide the filter input. Hiding also clears the active filter, so
53
57
  * the user never returns to an "invisible" filter narrowing the rows. */
54
58
  private _toggleFilterBar;
@@ -90,9 +94,9 @@ export declare class AssistantSessionsPanel extends Widget {
90
94
  private _logError;
91
95
  private _fetch;
92
96
  private _toggleFavourite;
93
- /** The warning a permanent deletion earns and a trashed one does not - a
94
- * trashed conversation is recoverable, and the popup one click away says
95
- * so; the same sentence on both would be false half the time. */
97
+ /** The warning a deletion earns: with the trash setting on, a refused move
98
+ * may remove the conversations permanently or leave them in place, so
99
+ * neither the verb nor the note may promise one outcome. */
96
100
  private get _irreversibleNote();
97
101
  private get _disposalVerb();
98
102
  private _removeProject;
@@ -307,8 +311,10 @@ export declare class AssistantSessionsPanel extends Widget {
307
311
  private _bodyEl;
308
312
  private _loadingEl;
309
313
  private _busyEl;
314
+ private _busyLabelEl;
310
315
  private _busyTextEl;
311
- private _busyCount;
316
+ private _busySeq;
317
+ private _busyMessages;
312
318
  private _errorEl;
313
319
  private _refreshBtn;
314
320
  private _filterBtn;
package/lib/core/panel.js CHANGED
@@ -113,8 +113,10 @@ export class AssistantSessionsPanel extends Widget {
113
113
  this._removingPaths = new Set();
114
114
  this._loadingEl = null;
115
115
  this._busyEl = null;
116
+ this._busyLabelEl = null;
116
117
  this._busyTextEl = null;
117
- this._busyCount = 0;
118
+ this._busySeq = 0;
119
+ this._busyMessages = new Map();
118
120
  this._errorEl = null;
119
121
  this._refreshBtn = null;
120
122
  this._filterBtn = null;
@@ -186,7 +188,13 @@ export class AssistantSessionsPanel extends Widget {
186
188
  return this._descriptor;
187
189
  }
188
190
  dispose() {
191
+ var _a;
189
192
  this._stopPolling();
193
+ // The busy overlay lives on the shell, outside this widget's tree, so
194
+ // removing the panel does not remove it. Left behind it would show a
195
+ // spinner for a panel that no longer exists.
196
+ (_a = this._busyEl) === null || _a === void 0 ? void 0 : _a.remove();
197
+ this._busyEl = null;
190
198
  // Disabling a provider must leave its running terminals alive and only
191
199
  // drop the tint this panel applied.
192
200
  void this._terminals.clearColours();
@@ -394,17 +402,28 @@ export class AssistantSessionsPanel extends Widget {
394
402
  loading.appendChild(loadingSpinner);
395
403
  // Click-time busy overlay: raised synchronously when a row is opened or
396
404
  // branched, before the terminal probe or the branch request, so the click
397
- // never reads as ignored (DEF-PANE-318). Unlike the refresh veil it takes
398
- // no pointer events and no focus: the context menu that launched a branch
399
- // stays open until its command runs, and a focus change would close it.
405
+ // never reads as ignored (DEF-PANE-318). It sits on the LAB SHELL, not on
406
+ // the panel: the feedback belongs in the main working area, where the
407
+ // launch spinner appears, and a panel that is hidden or scrolled must not
408
+ // keep it out of sight (DEF-PANE-318-1). It takes no pointer events and no
409
+ // focus: the context menu that launched a branch stays open until its
410
+ // command runs, and a focus change would close it.
400
411
  const busy = document.createElement('div');
401
- busy.className = 'jp-AiAssistantsPanel-busyOverlay';
412
+ busy.className = 'jp-AiAssistants-busyOverlay';
402
413
  busy.hidden = true;
403
414
  const busySpinner = document.createElement('div');
404
415
  busySpinner.className =
405
- 'jp-aiAssistants-spinner jp-AiAssistantsPanel-busySpinner';
416
+ 'jp-aiAssistants-spinner jp-AiAssistants-busySpinner';
406
417
  busySpinner.setAttribute('aria-hidden', 'true');
407
418
  busy.appendChild(busySpinner);
419
+ // The action, named in sight: under a reduced-motion preference the ring
420
+ // is held still, so the label carries the busy state with no animation.
421
+ // Hidden from assistive technology because the live region below
422
+ // announces the same words once.
423
+ const busyLabel = document.createElement('div');
424
+ busyLabel.className = 'jp-AiAssistants-busyLabel';
425
+ busyLabel.setAttribute('aria-hidden', 'true');
426
+ busy.appendChild(busyLabel);
408
427
  // The spinner is decoration; a screen-reader user hears the action
409
428
  // instead. The region sits on the root, always rendered and outside the
410
429
  // overlay and the aria-busy body: a live region created inside a
@@ -428,34 +447,47 @@ export class AssistantSessionsPanel extends Widget {
428
447
  root.appendChild(error);
429
448
  root.appendChild(body);
430
449
  root.appendChild(loading);
431
- root.appendChild(busy);
432
450
  root.appendChild(busyText);
451
+ // The shell carries the visible feedback; the panel carries the
452
+ // announcement.
453
+ this._app.shell.node.appendChild(busy);
433
454
  this._bodyEl = body;
434
455
  this._loadingEl = loading;
435
456
  this._busyEl = busy;
457
+ this._busyLabelEl = busyLabel;
436
458
  this._busyTextEl = busyText;
437
459
  this._errorEl = error;
438
460
  }
439
- /** Raise the click-time busy overlay. Counted, because an open and a branch
440
- * can overlap; the overlay clears when the last one ends. */
461
+ /** Raise the click-time busy overlay. Each action gets a token, because an
462
+ * open and a branch can overlap; the overlay clears when the last one ends,
463
+ * and the label names the newest action still running. */
441
464
  _beginBusy(message) {
442
- this._busyCount += 1;
465
+ const token = ++this._busySeq;
466
+ this._busyMessages.set(token, message);
443
467
  if (this._busyEl) {
444
468
  this._busyEl.hidden = false;
445
- if (this._busyTextEl) {
446
- this._busyTextEl.textContent = message;
447
- }
469
+ this._paintBusyMessage();
448
470
  // The body is what the action changes; the root stays outside the
449
471
  // busy subtree so the live region above is not withheld.
450
472
  if (this._bodyEl) {
451
473
  this._bodyEl.setAttribute('aria-busy', 'true');
452
474
  }
453
475
  }
476
+ return token;
454
477
  }
455
- _endBusy() {
456
- this._busyCount = Math.max(0, this._busyCount - 1);
457
- if (this._busyCount === 0 && this._busyEl) {
478
+ _endBusy(token) {
479
+ this._busyMessages.delete(token);
480
+ if (this._busyMessages.size > 0) {
481
+ // Another action is still running; neither the visible label nor the
482
+ // live region may keep naming the one that just ended.
483
+ this._paintBusyMessage();
484
+ return;
485
+ }
486
+ if (this._busyEl) {
458
487
  this._busyEl.hidden = true;
488
+ if (this._busyLabelEl) {
489
+ this._busyLabelEl.textContent = '';
490
+ }
459
491
  if (this._busyTextEl) {
460
492
  this._busyTextEl.textContent = '';
461
493
  }
@@ -464,6 +496,20 @@ export class AssistantSessionsPanel extends Widget {
464
496
  }
465
497
  }
466
498
  }
499
+ /** Name the newest still-running action in the visible label and the live
500
+ * region, so neither keeps naming an action that ended. */
501
+ _paintBusyMessage() {
502
+ let message = '';
503
+ for (const active of this._busyMessages.values()) {
504
+ message = active;
505
+ }
506
+ if (this._busyLabelEl) {
507
+ this._busyLabelEl.textContent = message;
508
+ }
509
+ if (this._busyTextEl) {
510
+ this._busyTextEl.textContent = message;
511
+ }
512
+ }
467
513
  /** Show / hide the filter input. Hiding also clears the active filter, so
468
514
  * the user never returns to an "invisible" filter narrowing the rows. */
469
515
  _toggleFilterBar() {
@@ -663,14 +709,18 @@ export class AssistantSessionsPanel extends Widget {
663
709
  this._showActionError('Could not toggle favorite - try again.', err);
664
710
  }
665
711
  }
666
- /** The warning a permanent deletion earns and a trashed one does not - a
667
- * trashed conversation is recoverable, and the popup one click away says
668
- * so; the same sentence on both would be false half the time. */
712
+ /** The warning a deletion earns: with the trash setting on, a refused move
713
+ * may remove the conversations permanently or leave them in place, so
714
+ * neither the verb nor the note may promise one outcome. */
669
715
  get _irreversibleNote() {
670
- return this._deleteToTrash ? '' : ' This cannot be undone.';
716
+ return this._deleteToTrash
717
+ ? ' A permanent removal cannot be undone.'
718
+ : ' This cannot be undone.';
671
719
  }
672
720
  get _disposalVerb() {
673
- return this._deleteToTrash ? 'moved to trash' : 'deleted permanently';
721
+ return this._deleteToTrash
722
+ ? 'moved to the trash when the system accepts them; a refused move may remove them permanently or leave them in place'
723
+ : 'deleted permanently';
674
724
  }
675
725
  async _removeProject(session) {
676
726
  var _a;
@@ -805,7 +855,7 @@ export class AssistantSessionsPanel extends Widget {
805
855
  async _openSession(session, force, sessionId) {
806
856
  // The click's own feedback, raised before the terminal walk and the
807
857
  // launch - both can take seconds (DEF-PANE-318).
808
- this._beginBusy(this._trans.__('Opening conversation'));
858
+ const busy = this._beginBusy(this._trans.__('Opening conversation'));
809
859
  try {
810
860
  await this._terminals.openSession(session, this._launchMode(force), sessionId);
811
861
  // Not awaited: the open has succeeded, and the sync is not part of it.
@@ -820,7 +870,7 @@ export class AssistantSessionsPanel extends Widget {
820
870
  }
821
871
  }
822
872
  finally {
823
- this._endBusy();
873
+ this._endBusy(busy);
824
874
  // Reuse or fresh launch, either way the picture changed. Pull fresh
825
875
  // state; a poll tick retries if this one fails.
826
876
  void this._fetch().catch(() => undefined);
@@ -955,7 +1005,10 @@ export class AssistantSessionsPanel extends Widget {
955
1005
  if (row) {
956
1006
  // A terminal already holding that conversation IS the answer: a second
957
1007
  // process on one history is never what the click meant (ACC-LNCH-152).
958
- const found = await this._terminals.findForSession(row.session_id);
1008
+ // The folder is required as well: a transcript re-homed into another
1009
+ // project keeps its id, and that terminal is not this folder's
1010
+ // (DEF-PANE-319).
1011
+ const found = await this._terminals.findForSession(row.session_id, folder);
959
1012
  if (found) {
960
1013
  this._terminals.focus(found.widget);
961
1014
  return found.widget;
@@ -1049,12 +1102,12 @@ export class AssistantSessionsPanel extends Widget {
1049
1102
  // Raised on the click: the branch POST mints the fork server-side (for
1050
1103
  // Claude and Codex by running their own CLI) and can take seconds
1051
1104
  // (DEF-PANE-318).
1052
- this._beginBusy(this._trans.__('Branching conversation'));
1105
+ const busy = this._beginBusy(this._trans.__('Branching conversation'));
1053
1106
  try {
1054
1107
  await this._branchByServerCopy(session, undefined, force);
1055
1108
  }
1056
1109
  finally {
1057
- this._endBusy();
1110
+ this._endBusy(busy);
1058
1111
  }
1059
1112
  }
1060
1113
  /** The server mints the new id and answers with it before the terminal
@@ -1217,7 +1270,6 @@ export class AssistantSessionsPanel extends Widget {
1217
1270
  }
1218
1271
  /** Make another conversation the row's current one. */
1219
1272
  async _switchBranch(session, sessionId) {
1220
- var _a;
1221
1273
  try {
1222
1274
  const result = await requestProvider(this._descriptor.id, 'switch', this._serverSettings, {
1223
1275
  method: 'POST',
@@ -1229,15 +1281,6 @@ export class AssistantSessionsPanel extends Widget {
1229
1281
  if (result.current !== result.requested) {
1230
1282
  Notification.warning('That conversation could not be made the current one.', { autoClose: 4000 });
1231
1283
  }
1232
- else {
1233
- // The re-render is the only other signal, and it can read as the
1234
- // opposite of success: the row now reports the conversation's true
1235
- // last activity, so switching to an old one dims the row and drops
1236
- // it out of Recent (DEF-PANE-181).
1237
- const target = this._lastBranches.find(b => b.session_id === sessionId);
1238
- const label = (_a = target === null || target === void 0 ? void 0 : target.label) !== null && _a !== void 0 ? _a : shortSessionId(sessionId, this._descriptor.sessionIdPrefix);
1239
- Notification.success(`${this._lookupName(session)}: switched to ${label}`, { autoClose: 3000 });
1240
- }
1241
1284
  }
1242
1285
  catch (err) {
1243
1286
  Notification.error(isResponseStatus(err, 404)
package/lib/core/popup.js CHANGED
@@ -44,9 +44,9 @@ export function showManageSessionsPopup(options) {
44
44
  let items = [...options.branches];
45
45
  const selected = new Set();
46
46
  let deleting = false; // guards the async delete against double invocation
47
- // Permanent deletion arms first and executes on the second click; a trashed
48
- // deletion is recoverable and goes straight through.
49
- const armsFirst = !options.deleteToTrash;
47
+ // Every deletion arms first and executes on the second click: the trash
48
+ // setting does not promise the system will accept the move, and a fallback
49
+ // removal is permanent.
50
50
  let armed = false;
51
51
  let armTimer = 0;
52
52
  const body = document.createElement('div');
@@ -87,7 +87,7 @@ export function showManageSessionsPopup(options) {
87
87
  deleteBtn.type = 'button';
88
88
  deleteBtn.className = 'jp-AiAssistantsPanel-branchDelete';
89
89
  deleteBtn.title = options.deleteToTrash
90
- ? 'Deleted conversations move to the trash (recoverable)'
90
+ ? 'Deleted conversations move to the trash when the system accepts them; a refused move may remove them permanently or leave them in place'
91
91
  : 'Deleted conversations are removed permanently';
92
92
  footer.appendChild(deleteBtn);
93
93
  // Plain visual counter (selection, or the last action), beside the button
@@ -336,12 +336,12 @@ export function showManageSessionsPopup(options) {
336
336
  return;
337
337
  }
338
338
  // No confirmation DIALOG here: a second Lumino dialog stacked on this popup
339
- // renders detached. A permanent deletion still confirms, in the footer
340
- // itself - the first click arms the button, the second executes, and any
341
- // selection change or four seconds of hesitation disarms it. A trashed
342
- // deletion is recoverable, so it keeps going through on one click, with
343
- // the live "N deleted" status and a toast from the caller on failure.
344
- if (armsFirst && !armed) {
339
+ // renders detached. The deletion confirms in the footer itself - the first
340
+ // click arms the button, the second executes, and any selection change or
341
+ // four seconds of hesitation disarms it. Every deletion arms, because the
342
+ // trash setting does not promise the system will accept the move, and a
343
+ // refused move may remove the conversation permanently or leave it.
344
+ if (!armed) {
345
345
  armed = true;
346
346
  window.clearTimeout(armTimer);
347
347
  armTimer = window.setTimeout(() => {
@@ -384,9 +384,9 @@ export function showManageSessionsPopup(options) {
384
384
  (_a = options.onSync) === null || _a === void 0 ? void 0 : _a.call(options, items);
385
385
  render();
386
386
  updateControls();
387
- const moved = options.deleteToTrash
388
- ? `${deleted} moved to trash`
389
- : `${deleted} deleted`;
387
+ // One wording for both outcomes: the client cannot know whether the
388
+ // trash accepted the move, so it does not claim it did.
389
+ const moved = `${deleted} deleted`;
390
390
  selCount.textContent = moved;
391
391
  selCount.classList.add('jp-mod-deleted');
392
392
  // Clear then re-set on the next tick so an identical count (two single
@@ -56,18 +56,26 @@ export declare class TerminalManager {
56
56
  * widget is attached and visible.
57
57
  */
58
58
  focus(widget: any): void;
59
- /** Find an open terminal running exactly this conversation, or null.
59
+ /** Find an open terminal running exactly this conversation in this project,
60
+ * or null.
60
61
  *
61
- * Matches on the conversation id ALONE. The server reads the true id from
62
- * the running process, so a session the user started by hand is recognised
63
- * too, not only launches that carry an argv id. An id is globally unique to
64
- * one conversation, so a terminal holding it IS the one to focus, whatever
65
- * cwd it reports - a process that changed directory, or whose project dir
66
- * was recreated, must still be reused rather than duplicated. */
67
- findForSession(wantedSessionId: string | undefined): Promise<{
62
+ * The server reads the true id from the running process, so a session the
63
+ * user started by hand is recognised too, not only launches that carry an
64
+ * argv id. The terminal must ALSO work in the requested project. An id is
65
+ * not globally unique in the store: Claude re-homes a transcript into a
66
+ * second project under the same id, so an id match alone can name a process
67
+ * running it from another tree, and reusing that one opens nothing in the
68
+ * folder the user clicked (DEF-PANE-319). A process that changed directory
69
+ * WITHIN the project still matches - the probe lists every cwd in its
70
+ * tree. */
71
+ findForSession(wantedSessionId: string | undefined, projectPath: string): Promise<{
68
72
  widget: any;
69
73
  runningId: string | null;
70
74
  } | null>;
75
+ /** Whether any process in the terminal's tree works in `projectPath` or a
76
+ * directory under it. No cwds means nothing to prove the terminal belongs
77
+ * to this project, so it is not reused. */
78
+ private _runsInProject;
71
79
  /** Ask the server what a terminal is running. Null when the widget has no
72
80
  * session name yet, or the probe fails - a terminal can vanish between
73
81
  * enumeration and probe, which is a retry, not an error. */
@@ -184,8 +184,9 @@ export class TerminalManager {
184
184
  this.focus(cached.widget);
185
185
  return;
186
186
  }
187
- // 2. Walk every live terminal, asking the server what each is running.
188
- const found = await this.findForSession(wanted);
187
+ // 2. Walk every live terminal, asking the server what each is running in
188
+ // this project.
189
+ const found = await this.findForSession(wanted, session.project_path);
189
190
  if (found) {
190
191
  // Tag with the OBSERVED conversation, so a later reuse trusts a
191
192
  // confirmed identity rather than a wish.
@@ -255,26 +256,43 @@ export class TerminalManager {
255
256
  }
256
257
  });
257
258
  }
258
- /** Find an open terminal running exactly this conversation, or null.
259
+ /** Find an open terminal running exactly this conversation in this project,
260
+ * or null.
259
261
  *
260
- * Matches on the conversation id ALONE. The server reads the true id from
261
- * the running process, so a session the user started by hand is recognised
262
- * too, not only launches that carry an argv id. An id is globally unique to
263
- * one conversation, so a terminal holding it IS the one to focus, whatever
264
- * cwd it reports - a process that changed directory, or whose project dir
265
- * was recreated, must still be reused rather than duplicated. */
266
- async findForSession(wantedSessionId) {
262
+ * The server reads the true id from the running process, so a session the
263
+ * user started by hand is recognised too, not only launches that carry an
264
+ * argv id. The terminal must ALSO work in the requested project. An id is
265
+ * not globally unique in the store: Claude re-homes a transcript into a
266
+ * second project under the same id, so an id match alone can name a process
267
+ * running it from another tree, and reusing that one opens nothing in the
268
+ * folder the user clicked (DEF-PANE-319). A process that changed directory
269
+ * WITHIN the project still matches - the probe lists every cwd in its
270
+ * tree. */
271
+ async findForSession(wantedSessionId, projectPath) {
267
272
  if (!this._tracker || !wantedSessionId) {
268
273
  return null;
269
274
  }
270
275
  for (const widget of this._liveTerminals()) {
271
276
  const info = await this.probe(widget);
272
- if (info && info.session_id && info.session_id === wantedSessionId) {
277
+ if (info &&
278
+ info.session_id &&
279
+ info.session_id === wantedSessionId &&
280
+ this._runsInProject(info.cwds, projectPath)) {
273
281
  return { widget, runningId: info.session_id };
274
282
  }
275
283
  }
276
284
  return null;
277
285
  }
286
+ /** Whether any process in the terminal's tree works in `projectPath` or a
287
+ * directory under it. No cwds means nothing to prove the terminal belongs
288
+ * to this project, so it is not reused. */
289
+ _runsInProject(cwds, projectPath) {
290
+ const root = projectPath.replace(/\/+$/, '');
291
+ return (cwds !== null && cwds !== void 0 ? cwds : []).some(cwd => {
292
+ const path = cwd.replace(/\/+$/, '');
293
+ return path === root || path.startsWith(`${root}/`);
294
+ });
295
+ }
278
296
  /** Ask the server what a terminal is running. Null when the widget has no
279
297
  * session name yet, or the probe fails - a terminal can vanish between
280
298
  * enumeration and probe, which is a retry, not an error. */
package/lib/index.js CHANGED
@@ -174,9 +174,9 @@ const plugin = {
174
174
  descriptor: module.descriptor,
175
175
  hooks: module.hooks,
176
176
  rootDir: (_a = status === null || status === void 0 ? void 0 : status.root_dir) !== null && _a !== void 0 ? _a : '',
177
- // Unknown reads as "not trash": the popup then arms its Delete
178
- // button before it executes, which is the only confirmation a
179
- // permanent bulk delete has (DEF-135).
177
+ // Unknown reads as "not trash", so the dialogs and the popup
178
+ // present the permanent-delete wording rather than promising a
179
+ // recoverable move the server never confirmed (DEF-135).
180
180
  deleteToTrash: status ? status.delete_to_trash !== false : false,
181
181
  terminalTracker,
182
182
  fileBrowser,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_ai_code_assistants_extension",
3
- "version": "1.2.99",
3
+ "version": "1.2.115",
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",
@@ -110,7 +110,8 @@ describe('DEF-GUARD-139 - reconcile hands a late roster to a docked panel', () =
110
110
  const app = {
111
111
  serviceManager: { serverSettings: {} },
112
112
  commands: new CommandRegistry(),
113
- hasPlugin: () => false
113
+ hasPlugin: () => false,
114
+ shell: { node: document.createElement('div') }
114
115
  } as any;
115
116
  const settings = {
116
117
  raw: '{}',
@@ -213,7 +214,8 @@ describe('ACC-LNCH-143..146 - the Launcher tile has the panel lifecycle', () =>
213
214
  const app = {
214
215
  serviceManager: { serverSettings: {} },
215
216
  commands: new CommandRegistry(),
216
- hasPlugin: () => false
217
+ hasPlugin: () => false,
218
+ shell: { node: document.createElement('div') }
217
219
  } as any;
218
220
  const labShell = {
219
221
  add: (widget: AssistantSessionsPanel) => docked.push(widget)
@@ -288,7 +290,8 @@ describe('ACC-LNCH-143..146 - the Launcher tile has the panel lifecycle', () =>
288
290
  {
289
291
  serviceManager: { serverSettings: {} },
290
292
  commands: new CommandRegistry(),
291
- hasPlugin: () => false
293
+ hasPlugin: () => false,
294
+ shell: { node: document.createElement('div') }
292
295
  } as any,
293
296
  { add: (widget: AssistantSessionsPanel) => docked.push(widget) } as any,
294
297
  null,
@@ -416,7 +419,8 @@ describe('ACC-PROV - the activation contract', () => {
416
419
  const app = {
417
420
  serviceManager: { serverSettings },
418
421
  commands: new CommandRegistry(),
419
- hasPlugin: () => false
422
+ hasPlugin: () => false,
423
+ shell: { node: document.createElement('div') }
420
424
  } as any;
421
425
 
422
426
  const composite = options.composite;
@@ -878,6 +882,8 @@ describe('ACC-SETT - settings drive the panels', () => {
878
882
  const shellMoves: [string, string][] = [];
879
883
  const sequence: string[] = [];
880
884
  const labShell = {
885
+ // The panel mounts its click-time overlay on the shell node.
886
+ node: document.createElement('div'),
881
887
  // As the real sidebar does: a widget is hidden when it is added, and a
882
888
  // placement the user made with JupyterLab's own command overrides the
883
889
  // area the caller asks for.
@@ -934,7 +940,8 @@ describe('ACC-SETT - settings drive the panels', () => {
934
940
  const app = {
935
941
  serviceManager: { serverSettings },
936
942
  commands,
937
- hasPlugin: () => false
943
+ hasPlugin: () => false,
944
+ shell: { node: document.createElement('div') }
938
945
  } as any;
939
946
 
940
947
  const changed = new Signal<unknown, void>({});
@@ -1758,7 +1765,8 @@ describe('ACC-RETI-82..84 - a retired standalone extension still installed', ()
1758
1765
  const app = {
1759
1766
  serviceManager: { serverSettings },
1760
1767
  commands: new CommandRegistry(),
1761
- hasPlugin: (id: string) => installed.includes(id)
1768
+ hasPlugin: (id: string) => installed.includes(id),
1769
+ shell: { node: document.createElement('div') }
1762
1770
  } as any;
1763
1771
  await (plugin.activate as any)(
1764
1772
  app,
@@ -192,7 +192,10 @@ function harness(
192
192
  }
193
193
  },
194
194
  commands,
195
- shell: { activateById: (id: string) => activated.push(id) }
195
+ shell: {
196
+ node: document.createElement('div'),
197
+ activateById: (id: string) => activated.push(id)
198
+ }
196
199
  } as any;
197
200
  const panel = new AssistantSessionsPanel({
198
201
  app,
@@ -279,7 +282,8 @@ describe('the tile click path resumes a folder that has a conversation', () => {
279
282
  'terminal/term-1': {
280
283
  terminal_name: 'term-1',
281
284
  running: true,
282
- session_id: 'sid-proj'
285
+ session_id: 'sid-proj',
286
+ cwds: ['/srv/lab/proj']
283
287
  }
284
288
  });
285
289
  const h = harness({ tracker: true });
@@ -291,6 +295,32 @@ describe('the tile click path resumes a folder that has a conversation', () => {
291
295
  expect(callsTo('launch-argv')).toHaveLength(0);
292
296
  });
293
297
 
298
+ it('launches here when the terminal holds the conversation in another folder', async () => {
299
+ // DEF-PANE-319: a transcript re-homed into another project keeps its id,
300
+ // so the id alone cannot prove the terminal is this folder's.
301
+ routes({
302
+ sessions: { sessions: [session()] },
303
+ 'terminal/term-1': {
304
+ terminal_name: 'term-1',
305
+ running: true,
306
+ session_id: 'sid-proj',
307
+ cwds: ['/srv/other']
308
+ },
309
+ 'launch-argv': { argv: ['/bin/testbed', '--resume', 'sid-proj'] }
310
+ });
311
+ const h = harness({ tracker: true });
312
+ await flush();
313
+
314
+ expect(await h.panel.launchHere('proj')).toBe(LAUNCHED_WIDGET);
315
+ expect(h.activated).not.toContain('terminal-open-1');
316
+ expect(h.launches).toEqual([
317
+ {
318
+ argv: ['/bin/testbed', '--resume', 'sid-proj'],
319
+ cwd: '/srv/lab/proj'
320
+ }
321
+ ]);
322
+ });
323
+
294
324
  it('asks for resume argv when no terminal holds it, and launches that', async () => {
295
325
  // ACC-LNCH-151 / ACC-LNCH-154: the row's own conversation, resumed through
296
326
  // the sibling's terminal.
@@ -489,7 +519,8 @@ describe('the tiles the plugin adds to the Launcher', () => {
489
519
  const app = {
490
520
  serviceManager: { serverSettings: {} },
491
521
  commands: new CommandRegistry(),
492
- hasPlugin: () => false
522
+ hasPlugin: () => false,
523
+ shell: { node: document.createElement('div') }
493
524
  } as any;
494
525
  const labShell = {
495
526
  add: (widget: AssistantSessionsPanel) => docked.push(widget)