jupyterlab_ai_code_assistants_extension 1.2.35 → 1.2.41

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
@@ -41,7 +41,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
41
41
  - **Favorites** - star projects you keep coming back to via the right-click menu; favourites from the standalone extensions are migrated on first run
42
42
  - **Remove and clean up** - drop a project's history or a project's extra parallel sessions from the right-click menu, confirmation dialog first; removed files honour JupyterLab's "move files to trash" setting
43
43
  - **Activity at a glance** - each row shows its last activity in an aligned column; rows active within the last minute light up, rows idle for over a week dim, and rows with parallel conversations show a branch icon with the count
44
- - **Remote control indicator and background agents** (Claude) - a green dot marks sessions actively under remote control, and a conversation held by a running background agent shows a `bg` chip; clicking attaches to the agent instead of copying it
44
+ - **Remote control indicator and background agents** (Claude) - a green dot marks sessions actively under remote control, and a conversation held by a running background agent shows a `bg` chip; clicking stops the agent and resumes the same conversation in your terminal, so it becomes an ordinary interactive session
45
45
  - **Search** - fuzzy filter per panel, toggled by the funnel button
46
46
  - **Presentation modes** - label rows by session name or by path relative to the JupyterLab root
47
47
  - **Conflict-safe upgrade** - if a retired standalone extension is still installed, its panel wins and this extension stands down for that assistant instead of showing a duplicate
@@ -252,24 +252,6 @@ export declare class AssistantSessionsPanel extends Widget {
252
252
  * that build a byte-identical launch, and the unmarked one is the default
253
253
  * the user clicks. */
254
254
  private _resolvedVariant;
255
- /** The same mode as it applies to RESUME - of the active row by default, or
256
- * of whichever conversation is named.
257
- *
258
- * An attach is not a launch: the server returns the argv before the flag is
259
- * appended, deliberately, so promising the mode on a conversation a live
260
- * worker already holds would name something the assistant never receives.
261
- * That is true of resuming and of nothing else, so it is tested HERE rather
262
- * than in `_resolvedVariant` - which is read by the `+` button and both
263
- * new-session and branch items, none of which attach. Testing it there let
264
- * one right-click on a background-agent row strip the mode's name off every
265
- * one of those surfaces panel-wide, while their launches went on carrying it
266
- * (DEF-37).
267
- *
268
- * The argument is what lets a surface that describes SOMEONE ELSE'S resume -
269
- * a row tooltip built during a render, a branch item in the submenu - ask the
270
- * question about that conversation rather than about the active row.
271
- */
272
- private _resumeVariant;
273
255
  /** The variant suffix a plain item wears while that mode is in force. */
274
256
  private _variantSuffix;
275
257
  /** Whether a forced variant would build the launch the plain item already
@@ -311,9 +293,7 @@ export declare class AssistantSessionsPanel extends Widget {
311
293
  private _openManagePopup;
312
294
  /** Tooltip for one Open button in the Manage Sessions popup. Same rule as the
313
295
  * Open Branched Conversation submenu: the button launches a terminal, so it
314
- * names the mode that launch carries, and a conversation a live worker holds
315
- * is attached to and carries none. The row's own conversation is not in
316
- * `_lastBranches`, so it answers from the session. */
296
+ * names the mode that launch carries. */
317
297
  private _openBranchTitle;
318
298
  /** The live-worker chip for a branch row, when the provider has workers at
319
299
  * all. Returning null lets the popup fall back to its fork marker. */
package/lib/core/panel.js CHANGED
@@ -1593,9 +1593,9 @@ export class AssistantSessionsPanel extends Widget {
1593
1593
  badge.appendChild(document.createTextNode(String(session.extra_sessions + 1)));
1594
1594
  name.appendChild(badge);
1595
1595
  }
1596
- // A live worker owns this conversation, so a click joins it rather than
1597
- // resuming a dormant one. Marked so the difference is visible before the
1598
- // click, not a surprise after it. No own title: a child title shadows the
1596
+ // A live worker owns this conversation, so a click stops it before
1597
+ // resuming. Marked so the difference is visible before the click, not a
1598
+ // surprise after it. No own title: a child title shadows the
1599
1599
  // row's, which would hide the path behind a two-character chip.
1600
1600
  if (this._descriptor.hasBgAgents && session.bg_id) {
1601
1601
  const bg = document.createElement('span');
@@ -1767,15 +1767,10 @@ export class AssistantSessionsPanel extends Widget {
1767
1767
  lines.push('Status: active');
1768
1768
  }
1769
1769
  if (this._descriptor.hasBgAgents && s.bg_id) {
1770
- lines.push(`Background agent: ${s.bg_id} (click attaches to it)`);
1770
+ lines.push(`Background agent: ${s.bg_id} (click stops it and resumes here)`);
1771
1771
  }
1772
1772
  lines.push(...((_c = (_b = (_a = this._hooks).tooltipLines) === null || _b === void 0 ? void 0 : _b.call(_a, s)) !== null && _c !== void 0 ? _c : []));
1773
- // What THIS row's click resolves to, which is the attach-aware question:
1774
- // the line above may have just said the click joins a live worker, and an
1775
- // attach is issued before the mode is appended, so `_resolvedVariant`
1776
- // promised a flag the server discards - one tooltip naming both, a line
1777
- // apart (DEF-46).
1778
- const variant = this._resumeVariant(s);
1773
+ const variant = this._resolvedVariant();
1779
1774
  if (variant) {
1780
1775
  // The row click launches with this, and the menu is the only other place
1781
1776
  // that says so.
@@ -1891,27 +1886,6 @@ export class AssistantSessionsPanel extends Widget {
1891
1886
  const resolved = resolvedLaunchModeEntry(this._descriptor.launchModes, this._modes);
1892
1887
  return resolved;
1893
1888
  }
1894
- /** The same mode as it applies to RESUME - of the active row by default, or
1895
- * of whichever conversation is named.
1896
- *
1897
- * An attach is not a launch: the server returns the argv before the flag is
1898
- * appended, deliberately, so promising the mode on a conversation a live
1899
- * worker already holds would name something the assistant never receives.
1900
- * That is true of resuming and of nothing else, so it is tested HERE rather
1901
- * than in `_resolvedVariant` - which is read by the `+` button and both
1902
- * new-session and branch items, none of which attach. Testing it there let
1903
- * one right-click on a background-agent row strip the mode's name off every
1904
- * one of those surfaces panel-wide, while their launches went on carrying it
1905
- * (DEF-37).
1906
- *
1907
- * The argument is what lets a surface that describes SOMEONE ELSE'S resume -
1908
- * a row tooltip built during a render, a branch item in the submenu - ask the
1909
- * question about that conversation rather than about the active row.
1910
- */
1911
- _resumeVariant(target = this._activeSession) {
1912
- const joining = this._descriptor.hasBgAgents && !!(target === null || target === void 0 ? void 0 : target.bg_id);
1913
- return joining ? null : this._resolvedVariant();
1914
- }
1915
1889
  /** The variant suffix a plain item wears while that mode is in force. */
1916
1890
  _variantSuffix() {
1917
1891
  const resolved = this._resolvedVariant();
@@ -1951,16 +1925,15 @@ export class AssistantSessionsPanel extends Widget {
1951
1925
  }
1952
1926
  });
1953
1927
  this._commands.addCommand(this._cmd('resume'), {
1954
- // Joining a live worker and resuming a dormant conversation are
1955
- // different verbs, so a provider that has both says which one this row
1956
- // gets.
1928
+ // Resuming a conversation a live worker holds stops that worker first,
1929
+ // so a provider that has workers says which of the two this row gets.
1957
1930
  label: () => {
1958
1931
  var _a, _b, _c;
1959
- const variant = this._resumeVariant();
1932
+ const variant = this._resolvedVariant();
1960
1933
  const verb = (_c = (_b = (_a = this._hooks).resumeLabel) === null || _b === void 0 ? void 0 : _b.call(_a, active())) !== null && _c !== void 0 ? _c : 'Resume';
1961
1934
  return variant ? `${verb} (${variant.label})` : verb;
1962
1935
  },
1963
- icon: () => { var _a; return (((_a = this._resumeVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? shieldIcon : undefined); },
1936
+ icon: () => { var _a; return (((_a = this._resolvedVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? shieldIcon : undefined); },
1964
1937
  execute: () => {
1965
1938
  const s = active();
1966
1939
  if (s) {
@@ -1970,15 +1943,13 @@ export class AssistantSessionsPanel extends Widget {
1970
1943
  });
1971
1944
  for (const mode of this._variantModes) {
1972
1945
  this._commands.addCommand(this._cmd(`resume-${mode.id}`), {
1973
- label: `Resume (${mode.menuLabel})`,
1946
+ // The same verb as the plain item, so a row a live worker holds says
1947
+ // here too that the worker is stopped first.
1948
+ label: () => { var _a, _b, _c; return `${(_c = (_b = (_a = this._hooks).resumeLabel) === null || _b === void 0 ? void 0 : _b.call(_a, active())) !== null && _c !== void 0 ? _c : 'Resume'} (${mode.menuLabel})`; },
1974
1949
  icon: shieldIcon,
1975
1950
  // Absent while the plain item already resolves to this same mode - it
1976
1951
  // would be a second entry building an identical launch.
1977
1952
  isVisible: () => !this._buildsSameLaunch(mode),
1978
- // A conversation a live worker already holds was started in whatever
1979
- // mode that worker has; joining it cannot change that, so the variant
1980
- // is disabled rather than silently joining in the wrong mode.
1981
- isEnabled: () => { var _a; return !(this._descriptor.hasBgAgents && ((_a = active()) === null || _a === void 0 ? void 0 : _a.bg_id)); },
1982
1953
  execute: () => {
1983
1954
  const s = active();
1984
1955
  if (s) {
@@ -2127,13 +2098,11 @@ export class AssistantSessionsPanel extends Widget {
2127
2098
  this._commands.addCommand(this._cmd('open-branch'), {
2128
2099
  // Names the mode the launch carries, like Resume, `+` and Branch Session
2129
2100
  // do - this item opens a terminal, so the mode in force applies to it and
2130
- // this was the one launch surface that stayed silent about it. Attach-
2131
- // aware per conversation: the submenu passes `bg`, since a branch a live
2132
- // worker holds is attached to and takes no mode (DEF-46's rule).
2101
+ // this was the one launch surface that stayed silent about it.
2133
2102
  label: args => {
2134
2103
  var _a;
2135
2104
  const base = String((_a = args.label) !== null && _a !== void 0 ? _a : '');
2136
- const variant = this._resumeVariant(args.bg ? { bg_id: String(args.bg) } : null);
2105
+ const variant = this._resolvedVariant();
2137
2106
  return variant ? `${base} (${variant.label})` : base;
2138
2107
  },
2139
2108
  // Intent glyph, not row state - see switch-branch above.
@@ -2436,10 +2405,7 @@ export class AssistantSessionsPanel extends Widget {
2436
2405
  command: this._cmd('open-branch'),
2437
2406
  args: {
2438
2407
  session_id: b.session_id,
2439
- label: this._branchMenuLabel(b),
2440
- // The label reads this to tell an attach from a launch; the
2441
- // provider's own worker capability is tested where it is answered.
2442
- ...(b.bg_id ? { bg: b.bg_id } : {})
2408
+ label: this._branchMenuLabel(b)
2443
2409
  }
2444
2410
  });
2445
2411
  }
@@ -2463,7 +2429,7 @@ export class AssistantSessionsPanel extends Widget {
2463
2429
  branchName: b => this._branchDisplayName(b),
2464
2430
  formatTime: ms => this._formatRelativeTime(ms),
2465
2431
  branchBadge: b => this._bgBadgeFor(b),
2466
- openTitle: id => this._openBranchTitle(session, id),
2432
+ openTitle: () => this._openBranchTitle(session),
2467
2433
  onSwitch: id => void this._switchBranch(session, id),
2468
2434
  onOpen: id => void this._openSession(session, undefined, id),
2469
2435
  onDelete: ids => this._deleteBranches(session, ids),
@@ -2475,21 +2441,16 @@ export class AssistantSessionsPanel extends Widget {
2475
2441
  }
2476
2442
  /** Tooltip for one Open button in the Manage Sessions popup. Same rule as the
2477
2443
  * Open Branched Conversation submenu: the button launches a terminal, so it
2478
- * names the mode that launch carries, and a conversation a live worker holds
2479
- * is attached to and carries none. The row's own conversation is not in
2480
- * `_lastBranches`, so it answers from the session. */
2481
- _openBranchTitle(session, sessionId) {
2482
- var _a, _b, _c, _d;
2444
+ * names the mode that launch carries. */
2445
+ _openBranchTitle(session) {
2446
+ var _a, _b, _c;
2483
2447
  // One terminal serves a project-scoped assistant, so the button focuses
2484
2448
  // or starts it, under the provider's own verb for that.
2485
2449
  if (this._descriptor.terminalScope === 'project') {
2486
2450
  return ((_c = (_b = (_a = this._hooks).resumeLabel) === null || _b === void 0 ? void 0 : _b.call(_a, session)) !== null && _c !== void 0 ? _c : "Open the project's terminal");
2487
2451
  }
2488
2452
  const base = 'Open this conversation in its own terminal';
2489
- const target = sessionId === session.session_id
2490
- ? session
2491
- : ((_d = this._lastBranches.find(b => b.session_id === sessionId)) !== null && _d !== void 0 ? _d : null);
2492
- const variant = this._resumeVariant(target);
2453
+ const variant = this._resolvedVariant();
2493
2454
  return variant ? `${base} (${variant.label})` : base;
2494
2455
  }
2495
2456
  /** The live-worker chip for a branch row, when the provider has workers at
@@ -41,8 +41,8 @@ function warnLegacyCompanion() {
41
41
  // And said where the user is looking. The symptom is tabs that are simply
42
42
  // plain, which reads as a feature that was never on rather than as something
43
43
  // to fix, and a console line only reaches someone already suspecting the
44
- // cause. The two neighbouring "an installed thing is wrong" announcements in
45
- // this extension both pair the console line with this toast. It does not
44
+ // cause. The retired-extension notice in index.ts pairs its console line
45
+ // with a toast the same way. It does not
46
46
  // close on its own, because the condition does not go away on its own.
47
47
  Notification.warning(message, { autoClose: false });
48
48
  }
@@ -309,7 +309,8 @@ export interface ITabColourEvents {
309
309
  * shared panel. */
310
310
  export interface IProviderHooks {
311
311
  /** Label for the primary open action, when the provider distinguishes verbs
312
- * (joining a live worker is not the same as resuming a dormant store entry). */
312
+ * (stopping a live worker before resuming is not the same as resuming a
313
+ * dormant store entry). */
313
314
  resumeLabel?: (session: ISession | null) => string;
314
315
  /** Extra lines for the row tooltip. */
315
316
  tooltipLines?: (session: ISession) => string[];
package/lib/index.js CHANGED
@@ -242,7 +242,7 @@ const plugin = {
242
242
  /** Bring the docked panels in line with settings and CLI availability.
243
243
  * Both must hold for a panel to be present. */
244
244
  const reconcile = () => {
245
- var _a, _b, _c, _d, _e;
245
+ var _a, _b, _c;
246
246
  for (const descriptor of registry.descriptors) {
247
247
  const id = descriptor.id;
248
248
  if (suppressed.has(id)) {
@@ -258,38 +258,26 @@ const plugin = {
258
258
  // the server's word on each binary is final.
259
259
  const roster = status;
260
260
  const available = roster === null ? true : probe !== null && probe.available !== false;
261
- // Enabled but binary absent must not be a silent state: the user sees
262
- // a missing panel with no clue that PATH under the Jupyter server
263
- // differs from their shell. Once per id, not per reconcile.
264
- // Only when the probe SUCCEEDED and this provider is the one missing.
265
- // Blaming PATH for a server the frontend never reached names the wrong
266
- // cause; and every provider defaults to enabled, so a machine with one
267
- // assistant installed would otherwise raise three toasts about assistants
268
- // the user has never heard of. The console line stays for that case.
269
- const chosen = (settings === null || settings === void 0 ? void 0 : settings.get(`providers.${id}.enabled`).user) !== undefined;
261
+ // Enabled but binary absent is logged to the console, never announced:
262
+ // enabled means the extension supports the assistant, not that it is
263
+ // installed, so a toast would name a tool the user never installed
264
+ // (DEF-PANE-277). Only when the probe SUCCEEDED and this provider is
265
+ // the one missing - blaming PATH for a server the frontend never
266
+ // reached names the wrong cause. Once per id, not per reconcile.
270
267
  if (probe !== null &&
271
268
  probe.available === false &&
272
269
  enabled &&
273
- chosen &&
274
270
  !warnedUnavailable.has(id)) {
275
271
  warnedUnavailable.add(id);
276
272
  const binary = (_b = (_a = registry.get(id)) === null || _a === void 0 ? void 0 : _a.descriptor.cliBinary) !== null && _b !== void 0 ? _b : id;
277
- const label = (_d = (_c = registry.get(id)) === null || _c === void 0 ? void 0 : _c.descriptor.label) !== null && _d !== void 0 ? _d : id;
278
273
  console.info(`${LOG_PREFIX} "${id}" is enabled but its \`${binary}\` binary was not found on the server's PATH; panel not shown.`);
279
- // And say it where the user is looking. A console line satisfies the
280
- // comment above only for someone who already suspects the cause; the
281
- // symptom is a panel that never appears, which reads as a broken
282
- // install rather than a PATH difference between the Jupyter server
283
- // and the user's shell. The retired-extension case next door already
284
- // gets a notification, so the COMMON failure was the quiet one.
285
- Notification.warning(`${label} is enabled but \`${binary}\` was not found on the Jupyter server's PATH, so its panel is not shown.`, { autoClose: 8000 });
286
274
  }
287
275
  // The latch is per ABSENCE, not per page load. Once the server reports
288
- // the binary present again, a later disappearance gets its own warning:
276
+ // the binary present again, a later disappearance gets its own line:
289
277
  // without this, one transient absence - a CLI upgrade unlinking the
290
278
  // binary for a moment, or an obsolete roster from an overlapping probe -
291
279
  // permanently suppresses the real one (DEF-125). It cannot make the
292
- // warning repeat while the state is unchanged, which is what the
280
+ // line repeat while the state is unchanged, which is what the
293
281
  // once-per-id latch above exists to prevent: the two conditions are
294
282
  // mutually exclusive.
295
283
  if (probe !== null && probe.available !== false) {
@@ -302,7 +290,7 @@ const plugin = {
302
290
  // A panel docked before the first roster was built without the
303
291
  // server root, and cannot resolve paths until it has one.
304
292
  if (roster !== null) {
305
- panel.setRoot((_e = roster.root_dir) !== null && _e !== void 0 ? _e : '', roster.delete_to_trash !== false);
293
+ panel.setRoot((_c = roster.root_dir) !== null && _c !== void 0 ? _c : '', roster.delete_to_trash !== false);
306
294
  }
307
295
  }
308
296
  else {
@@ -70,7 +70,7 @@ export const descriptor = {
70
70
  legacyPluginId: 'jupyterlab_claude_code_extension'
71
71
  };
72
72
  export const hooks = {
73
- // Joining a live agent and resuming a dormant conversation are different
74
- // verbs, so the item says which one this row gets before the click.
75
- resumeLabel: (session) => (session === null || session === void 0 ? void 0 : session.bg_id) ? 'Attach to Background Agent' : 'Resume'
73
+ // Opening a conversation a live agent holds stops the agent first, so the
74
+ // item says so before the click (DEF-PROV-272).
75
+ resumeLabel: (session) => (session === null || session === void 0 ? void 0 : session.bg_id) ? 'Stop Background Agent and Resume' : 'Resume'
76
76
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_ai_code_assistants_extension",
3
- "version": "1.2.35",
3
+ "version": "1.2.41",
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",
@@ -500,6 +500,40 @@ describe('ACC-PROV - the activation contract', () => {
500
500
  live.docked.forEach(widget => widget.dispose());
501
501
  });
502
502
 
503
+ it('DEF-PANE-277 - an enabled assistant whose binary is absent gets no panel and no toast', async () => {
504
+ const toasted = jest
505
+ .spyOn(Notification, 'warning')
506
+ .mockImplementation(() => '');
507
+ const logged = jest
508
+ .spyOn(console, 'info')
509
+ .mockImplementation(() => undefined);
510
+ const absent = PROVIDERS[1].descriptor.id;
511
+
512
+ // Saved explicitly, as the settings editor writes it. Enabled means the
513
+ // extension supports the assistant, not that it is installed.
514
+ const live = await activate({
515
+ composite: { [`providers.${absent}.enabled`]: true },
516
+ answers: [() => Promise.resolve(roster(absent))]
517
+ });
518
+
519
+ expect(
520
+ live.docked
521
+ .filter(widget => !widget.isDisposed)
522
+ .map(widget => widget.descriptor.id)
523
+ ).not.toContain(absent);
524
+ expect(toasted).not.toHaveBeenCalled();
525
+ // The reason stays in the browser console, once.
526
+ expect(
527
+ logged.mock.calls
528
+ .map(args => String(args[0]))
529
+ .filter(line => line.includes(`"${absent}"`))
530
+ ).toHaveLength(1);
531
+
532
+ toasted.mockRestore();
533
+ logged.mockRestore();
534
+ live.docked.forEach(widget => widget.dispose());
535
+ });
536
+
503
537
  it('ACC-PROV-16 - a binary installed after start gains its panel on the next probe', async () => {
504
538
  const late = PROVIDERS[2].descriptor.id;
505
539
  let present = false;
@@ -645,13 +645,15 @@ describe('DEF-46 - the row tooltip names only a mode the row launches with', ()
645
645
  expect(tooltip([session()])).toContain('Launch mode: Skip Permissions');
646
646
  });
647
647
 
648
- it('says nothing about a mode on a row a live worker already holds', () => {
648
+ it('names the mode on a row a live worker holds, which is resumed too', () => {
649
649
  panel.setModes({ skip: true });
650
- // An attach is issued before the flag is appended, deliberately, so the
651
- // tooltip would otherwise promise something the assistant never receives.
650
+ // The click stops the worker and resumes the conversation, and a resume
651
+ // carries the mode (DEF-PROV-272).
652
652
  const text = tooltip([session({ bg_id: 'abcd1234' })]);
653
- expect(text).toContain('Background agent: abcd1234 (click attaches to it)');
654
- expect(text).not.toContain('Launch mode');
653
+ expect(text).toContain(
654
+ 'Background agent: abcd1234 (click stops it and resumes here)'
655
+ );
656
+ expect(text).toContain('Launch mode: Skip Permissions');
655
657
  });
656
658
  });
657
659
 
package/src/core/panel.ts CHANGED
@@ -1899,9 +1899,9 @@ export class AssistantSessionsPanel extends Widget {
1899
1899
  );
1900
1900
  name.appendChild(badge);
1901
1901
  }
1902
- // A live worker owns this conversation, so a click joins it rather than
1903
- // resuming a dormant one. Marked so the difference is visible before the
1904
- // click, not a surprise after it. No own title: a child title shadows the
1902
+ // A live worker owns this conversation, so a click stops it before
1903
+ // resuming. Marked so the difference is visible before the click, not a
1904
+ // surprise after it. No own title: a child title shadows the
1905
1905
  // row's, which would hide the path behind a two-character chip.
1906
1906
  if (this._descriptor.hasBgAgents && session.bg_id) {
1907
1907
  const bg = document.createElement('span');
@@ -2086,15 +2086,12 @@ export class AssistantSessionsPanel extends Widget {
2086
2086
  lines.push('Status: active');
2087
2087
  }
2088
2088
  if (this._descriptor.hasBgAgents && s.bg_id) {
2089
- lines.push(`Background agent: ${s.bg_id} (click attaches to it)`);
2089
+ lines.push(
2090
+ `Background agent: ${s.bg_id} (click stops it and resumes here)`
2091
+ );
2090
2092
  }
2091
2093
  lines.push(...(this._hooks.tooltipLines?.(s) ?? []));
2092
- // What THIS row's click resolves to, which is the attach-aware question:
2093
- // the line above may have just said the click joins a live worker, and an
2094
- // attach is issued before the mode is appended, so `_resolvedVariant`
2095
- // promised a flag the server discards - one tooltip naming both, a line
2096
- // apart (DEF-46).
2097
- const variant = this._resumeVariant(s);
2094
+ const variant = this._resolvedVariant();
2098
2095
  if (variant) {
2099
2096
  // The row click launches with this, and the menu is the only other place
2100
2097
  // that says so.
@@ -2230,30 +2227,6 @@ export class AssistantSessionsPanel extends Widget {
2230
2227
  return resolved;
2231
2228
  }
2232
2229
 
2233
- /** The same mode as it applies to RESUME - of the active row by default, or
2234
- * of whichever conversation is named.
2235
- *
2236
- * An attach is not a launch: the server returns the argv before the flag is
2237
- * appended, deliberately, so promising the mode on a conversation a live
2238
- * worker already holds would name something the assistant never receives.
2239
- * That is true of resuming and of nothing else, so it is tested HERE rather
2240
- * than in `_resolvedVariant` - which is read by the `+` button and both
2241
- * new-session and branch items, none of which attach. Testing it there let
2242
- * one right-click on a background-agent row strip the mode's name off every
2243
- * one of those surfaces panel-wide, while their launches went on carrying it
2244
- * (DEF-37).
2245
- *
2246
- * The argument is what lets a surface that describes SOMEONE ELSE'S resume -
2247
- * a row tooltip built during a render, a branch item in the submenu - ask the
2248
- * question about that conversation rather than about the active row.
2249
- */
2250
- private _resumeVariant(
2251
- target: { bg_id?: string | null } | null = this._activeSession
2252
- ): IResolvedLaunchMode | null {
2253
- const joining = this._descriptor.hasBgAgents && !!target?.bg_id;
2254
- return joining ? null : this._resolvedVariant();
2255
- }
2256
-
2257
2230
  /** The variant suffix a plain item wears while that mode is in force. */
2258
2231
  private _variantSuffix(): string {
2259
2232
  const resolved = this._resolvedVariant();
@@ -2302,15 +2275,14 @@ export class AssistantSessionsPanel extends Widget {
2302
2275
  });
2303
2276
 
2304
2277
  this._commands.addCommand(this._cmd('resume'), {
2305
- // Joining a live worker and resuming a dormant conversation are
2306
- // different verbs, so a provider that has both says which one this row
2307
- // gets.
2278
+ // Resuming a conversation a live worker holds stops that worker first,
2279
+ // so a provider that has workers says which of the two this row gets.
2308
2280
  label: () => {
2309
- const variant = this._resumeVariant();
2281
+ const variant = this._resolvedVariant();
2310
2282
  const verb = this._hooks.resumeLabel?.(active()) ?? 'Resume';
2311
2283
  return variant ? `${verb} (${variant.label})` : verb;
2312
2284
  },
2313
- icon: () => (this._resumeVariant()?.unsafe ? shieldIcon : undefined),
2285
+ icon: () => (this._resolvedVariant()?.unsafe ? shieldIcon : undefined),
2314
2286
  execute: () => {
2315
2287
  const s = active();
2316
2288
  if (s) {
@@ -2321,15 +2293,14 @@ export class AssistantSessionsPanel extends Widget {
2321
2293
 
2322
2294
  for (const mode of this._variantModes) {
2323
2295
  this._commands.addCommand(this._cmd(`resume-${mode.id}`), {
2324
- label: `Resume (${mode.menuLabel})`,
2296
+ // The same verb as the plain item, so a row a live worker holds says
2297
+ // here too that the worker is stopped first.
2298
+ label: () =>
2299
+ `${this._hooks.resumeLabel?.(active()) ?? 'Resume'} (${mode.menuLabel})`,
2325
2300
  icon: shieldIcon,
2326
2301
  // Absent while the plain item already resolves to this same mode - it
2327
2302
  // would be a second entry building an identical launch.
2328
2303
  isVisible: () => !this._buildsSameLaunch(mode),
2329
- // A conversation a live worker already holds was started in whatever
2330
- // mode that worker has; joining it cannot change that, so the variant
2331
- // is disabled rather than silently joining in the wrong mode.
2332
- isEnabled: () => !(this._descriptor.hasBgAgents && active()?.bg_id),
2333
2304
  execute: () => {
2334
2305
  const s = active();
2335
2306
  if (s) {
@@ -2502,14 +2473,10 @@ export class AssistantSessionsPanel extends Widget {
2502
2473
  this._commands.addCommand(this._cmd('open-branch'), {
2503
2474
  // Names the mode the launch carries, like Resume, `+` and Branch Session
2504
2475
  // do - this item opens a terminal, so the mode in force applies to it and
2505
- // this was the one launch surface that stayed silent about it. Attach-
2506
- // aware per conversation: the submenu passes `bg`, since a branch a live
2507
- // worker holds is attached to and takes no mode (DEF-46's rule).
2476
+ // this was the one launch surface that stayed silent about it.
2508
2477
  label: args => {
2509
2478
  const base = String(args.label ?? '');
2510
- const variant = this._resumeVariant(
2511
- args.bg ? { bg_id: String(args.bg) } : null
2512
- );
2479
+ const variant = this._resolvedVariant();
2513
2480
  return variant ? `${base} (${variant.label})` : base;
2514
2481
  },
2515
2482
  // Intent glyph, not row state - see switch-branch above.
@@ -2844,10 +2811,7 @@ export class AssistantSessionsPanel extends Widget {
2844
2811
  command: this._cmd('open-branch'),
2845
2812
  args: {
2846
2813
  session_id: b.session_id,
2847
- label: this._branchMenuLabel(b),
2848
- // The label reads this to tell an attach from a launch; the
2849
- // provider's own worker capability is tested where it is answered.
2850
- ...(b.bg_id ? { bg: b.bg_id } : {})
2814
+ label: this._branchMenuLabel(b)
2851
2815
  }
2852
2816
  });
2853
2817
  }
@@ -2875,7 +2839,7 @@ export class AssistantSessionsPanel extends Widget {
2875
2839
  branchName: b => this._branchDisplayName(b),
2876
2840
  formatTime: ms => this._formatRelativeTime(ms),
2877
2841
  branchBadge: b => this._bgBadgeFor(b),
2878
- openTitle: id => this._openBranchTitle(session, id),
2842
+ openTitle: () => this._openBranchTitle(session),
2879
2843
  onSwitch: id => void this._switchBranch(session, id),
2880
2844
  onOpen: id => void this._openSession(session, undefined, id),
2881
2845
  onDelete: ids => this._deleteBranches(session, ids),
@@ -2888,10 +2852,8 @@ export class AssistantSessionsPanel extends Widget {
2888
2852
 
2889
2853
  /** Tooltip for one Open button in the Manage Sessions popup. Same rule as the
2890
2854
  * Open Branched Conversation submenu: the button launches a terminal, so it
2891
- * names the mode that launch carries, and a conversation a live worker holds
2892
- * is attached to and carries none. The row's own conversation is not in
2893
- * `_lastBranches`, so it answers from the session. */
2894
- private _openBranchTitle(session: ISession, sessionId: string): string {
2855
+ * names the mode that launch carries. */
2856
+ private _openBranchTitle(session: ISession): string {
2895
2857
  // One terminal serves a project-scoped assistant, so the button focuses
2896
2858
  // or starts it, under the provider's own verb for that.
2897
2859
  if (this._descriptor.terminalScope === 'project') {
@@ -2900,11 +2862,7 @@ export class AssistantSessionsPanel extends Widget {
2900
2862
  );
2901
2863
  }
2902
2864
  const base = 'Open this conversation in its own terminal';
2903
- const target =
2904
- sessionId === session.session_id
2905
- ? session
2906
- : (this._lastBranches.find(b => b.session_id === sessionId) ?? null);
2907
- const variant = this._resumeVariant(target);
2865
+ const variant = this._resolvedVariant();
2908
2866
  return variant ? `${base} (${variant.label})` : base;
2909
2867
  }
2910
2868
 
@@ -65,8 +65,8 @@ function warnLegacyCompanion(): void {
65
65
  // And said where the user is looking. The symptom is tabs that are simply
66
66
  // plain, which reads as a feature that was never on rather than as something
67
67
  // to fix, and a console line only reaches someone already suspecting the
68
- // cause. The two neighbouring "an installed thing is wrong" announcements in
69
- // this extension both pair the console line with this toast. It does not
68
+ // cause. The retired-extension notice in index.ts pairs its console line
69
+ // with a toast the same way. It does not
70
70
  // close on its own, because the condition does not go away on its own.
71
71
  Notification.warning(message, { autoClose: false });
72
72
  }
package/src/core/types.ts CHANGED
@@ -349,7 +349,8 @@ export interface ITabColourEvents {
349
349
  * shared panel. */
350
350
  export interface IProviderHooks {
351
351
  /** Label for the primary open action, when the provider distinguishes verbs
352
- * (joining a live worker is not the same as resuming a dormant store entry). */
352
+ * (stopping a live worker before resuming is not the same as resuming a
353
+ * dormant store entry). */
353
354
  resumeLabel?: (session: ISession | null) => string;
354
355
  /** Extra lines for the row tooltip. */
355
356
  tooltipLines?: (session: ISession) => string[];
package/src/index.ts CHANGED
@@ -333,46 +333,30 @@ const plugin: JupyterFrontEndPlugin<void> = {
333
333
  const roster = status;
334
334
  const available =
335
335
  roster === null ? true : probe !== null && probe.available !== false;
336
- // Enabled but binary absent must not be a silent state: the user sees
337
- // a missing panel with no clue that PATH under the Jupyter server
338
- // differs from their shell. Once per id, not per reconcile.
339
- // Only when the probe SUCCEEDED and this provider is the one missing.
340
- // Blaming PATH for a server the frontend never reached names the wrong
341
- // cause; and every provider defaults to enabled, so a machine with one
342
- // assistant installed would otherwise raise three toasts about assistants
343
- // the user has never heard of. The console line stays for that case.
344
- const chosen =
345
- settings?.get(`providers.${id}.enabled`).user !== undefined;
336
+ // Enabled but binary absent is logged to the console, never announced:
337
+ // enabled means the extension supports the assistant, not that it is
338
+ // installed, so a toast would name a tool the user never installed
339
+ // (DEF-PANE-277). Only when the probe SUCCEEDED and this provider is
340
+ // the one missing - blaming PATH for a server the frontend never
341
+ // reached names the wrong cause. Once per id, not per reconcile.
346
342
  if (
347
343
  probe !== null &&
348
344
  probe.available === false &&
349
345
  enabled &&
350
- chosen &&
351
346
  !warnedUnavailable.has(id)
352
347
  ) {
353
348
  warnedUnavailable.add(id);
354
349
  const binary = registry.get(id)?.descriptor.cliBinary ?? id;
355
- const label = registry.get(id)?.descriptor.label ?? id;
356
350
  console.info(
357
351
  `${LOG_PREFIX} "${id}" is enabled but its \`${binary}\` binary was not found on the server's PATH; panel not shown.`
358
352
  );
359
- // And say it where the user is looking. A console line satisfies the
360
- // comment above only for someone who already suspects the cause; the
361
- // symptom is a panel that never appears, which reads as a broken
362
- // install rather than a PATH difference between the Jupyter server
363
- // and the user's shell. The retired-extension case next door already
364
- // gets a notification, so the COMMON failure was the quiet one.
365
- Notification.warning(
366
- `${label} is enabled but \`${binary}\` was not found on the Jupyter server's PATH, so its panel is not shown.`,
367
- { autoClose: 8000 }
368
- );
369
353
  }
370
354
  // The latch is per ABSENCE, not per page load. Once the server reports
371
- // the binary present again, a later disappearance gets its own warning:
355
+ // the binary present again, a later disappearance gets its own line:
372
356
  // without this, one transient absence - a CLI upgrade unlinking the
373
357
  // binary for a moment, or an obsolete roster from an overlapping probe -
374
358
  // permanently suppresses the real one (DEF-125). It cannot make the
375
- // warning repeat while the state is unchanged, which is what the
359
+ // line repeat while the state is unchanged, which is what the
376
360
  // once-per-id latch above exists to prevent: the two conditions are
377
361
  // mutually exclusive.
378
362
  if (probe !== null && probe.available !== false) {
@@ -76,8 +76,8 @@ export const descriptor: IProviderDescriptor = {
76
76
  };
77
77
 
78
78
  export const hooks: IProviderHooks = {
79
- // Joining a live agent and resuming a dormant conversation are different
80
- // verbs, so the item says which one this row gets before the click.
79
+ // Opening a conversation a live agent holds stops the agent first, so the
80
+ // item says so before the click (DEF-PROV-272).
81
81
  resumeLabel: (session: ISession | null) =>
82
- session?.bg_id ? 'Attach to Background Agent' : 'Resume'
82
+ session?.bg_id ? 'Stop Background Agent and Resume' : 'Resume'
83
83
  };