jupyterlab_ai_code_assistants_extension 1.2.31 → 1.2.38

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');
@@ -1620,10 +1620,8 @@ export class AssistantSessionsPanel extends Widget {
1620
1620
  row.appendChild(this._renderIndicator(session));
1621
1621
  }
1622
1622
  // No star in the Favorites section - every row there is one by definition.
1623
- // The star sits before the time label and beside it: the label sizes to its
1624
- // own text, so the distance between the two is the row's gap on every row
1625
- // rather than however much shorter than "11mo ago" this label happens to
1626
- // be.
1623
+ // The star sits before the time so the minimum-width time column stays the
1624
+ // rightmost alignment anchor across all rows.
1627
1625
  if (session.favourite && sectionKey !== 'favourites') {
1628
1626
  // A marker, not a control. It carried a `title` that made it read as
1629
1627
  // one while a click fell through to the row and launched a terminal;
@@ -1636,8 +1634,8 @@ export class AssistantSessionsPanel extends Widget {
1636
1634
  starFilledIcon.element({ container: star });
1637
1635
  row.appendChild(star);
1638
1636
  }
1639
- // Always present, empty when the session carries no mtime, so every row
1640
- // has the same elements in the same order whatever the data holds.
1637
+ // Always present (empty without an mtime) so the star column keeps the
1638
+ // same anchor across every row in the panel.
1641
1639
  const time = document.createElement('span');
1642
1640
  time.className = 'jp-AiAssistantsPanel-rowTime';
1643
1641
  time.textContent = session.file_mtime
@@ -1769,15 +1767,10 @@ export class AssistantSessionsPanel extends Widget {
1769
1767
  lines.push('Status: active');
1770
1768
  }
1771
1769
  if (this._descriptor.hasBgAgents && s.bg_id) {
1772
- 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)`);
1773
1771
  }
1774
1772
  lines.push(...((_c = (_b = (_a = this._hooks).tooltipLines) === null || _b === void 0 ? void 0 : _b.call(_a, s)) !== null && _c !== void 0 ? _c : []));
1775
- // What THIS row's click resolves to, which is the attach-aware question:
1776
- // the line above may have just said the click joins a live worker, and an
1777
- // attach is issued before the mode is appended, so `_resolvedVariant`
1778
- // promised a flag the server discards - one tooltip naming both, a line
1779
- // apart (DEF-46).
1780
- const variant = this._resumeVariant(s);
1773
+ const variant = this._resolvedVariant();
1781
1774
  if (variant) {
1782
1775
  // The row click launches with this, and the menu is the only other place
1783
1776
  // that says so.
@@ -1893,27 +1886,6 @@ export class AssistantSessionsPanel extends Widget {
1893
1886
  const resolved = resolvedLaunchModeEntry(this._descriptor.launchModes, this._modes);
1894
1887
  return resolved;
1895
1888
  }
1896
- /** The same mode as it applies to RESUME - of the active row by default, or
1897
- * of whichever conversation is named.
1898
- *
1899
- * An attach is not a launch: the server returns the argv before the flag is
1900
- * appended, deliberately, so promising the mode on a conversation a live
1901
- * worker already holds would name something the assistant never receives.
1902
- * That is true of resuming and of nothing else, so it is tested HERE rather
1903
- * than in `_resolvedVariant` - which is read by the `+` button and both
1904
- * new-session and branch items, none of which attach. Testing it there let
1905
- * one right-click on a background-agent row strip the mode's name off every
1906
- * one of those surfaces panel-wide, while their launches went on carrying it
1907
- * (DEF-37).
1908
- *
1909
- * The argument is what lets a surface that describes SOMEONE ELSE'S resume -
1910
- * a row tooltip built during a render, a branch item in the submenu - ask the
1911
- * question about that conversation rather than about the active row.
1912
- */
1913
- _resumeVariant(target = this._activeSession) {
1914
- const joining = this._descriptor.hasBgAgents && !!(target === null || target === void 0 ? void 0 : target.bg_id);
1915
- return joining ? null : this._resolvedVariant();
1916
- }
1917
1889
  /** The variant suffix a plain item wears while that mode is in force. */
1918
1890
  _variantSuffix() {
1919
1891
  const resolved = this._resolvedVariant();
@@ -1953,16 +1925,15 @@ export class AssistantSessionsPanel extends Widget {
1953
1925
  }
1954
1926
  });
1955
1927
  this._commands.addCommand(this._cmd('resume'), {
1956
- // Joining a live worker and resuming a dormant conversation are
1957
- // different verbs, so a provider that has both says which one this row
1958
- // 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.
1959
1930
  label: () => {
1960
1931
  var _a, _b, _c;
1961
- const variant = this._resumeVariant();
1932
+ const variant = this._resolvedVariant();
1962
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';
1963
1934
  return variant ? `${verb} (${variant.label})` : verb;
1964
1935
  },
1965
- 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); },
1966
1937
  execute: () => {
1967
1938
  const s = active();
1968
1939
  if (s) {
@@ -1972,15 +1943,13 @@ export class AssistantSessionsPanel extends Widget {
1972
1943
  });
1973
1944
  for (const mode of this._variantModes) {
1974
1945
  this._commands.addCommand(this._cmd(`resume-${mode.id}`), {
1975
- 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})`; },
1976
1949
  icon: shieldIcon,
1977
1950
  // Absent while the plain item already resolves to this same mode - it
1978
1951
  // would be a second entry building an identical launch.
1979
1952
  isVisible: () => !this._buildsSameLaunch(mode),
1980
- // A conversation a live worker already holds was started in whatever
1981
- // mode that worker has; joining it cannot change that, so the variant
1982
- // is disabled rather than silently joining in the wrong mode.
1983
- isEnabled: () => { var _a; return !(this._descriptor.hasBgAgents && ((_a = active()) === null || _a === void 0 ? void 0 : _a.bg_id)); },
1984
1953
  execute: () => {
1985
1954
  const s = active();
1986
1955
  if (s) {
@@ -2129,13 +2098,11 @@ export class AssistantSessionsPanel extends Widget {
2129
2098
  this._commands.addCommand(this._cmd('open-branch'), {
2130
2099
  // Names the mode the launch carries, like Resume, `+` and Branch Session
2131
2100
  // do - this item opens a terminal, so the mode in force applies to it and
2132
- // this was the one launch surface that stayed silent about it. Attach-
2133
- // aware per conversation: the submenu passes `bg`, since a branch a live
2134
- // 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.
2135
2102
  label: args => {
2136
2103
  var _a;
2137
2104
  const base = String((_a = args.label) !== null && _a !== void 0 ? _a : '');
2138
- const variant = this._resumeVariant(args.bg ? { bg_id: String(args.bg) } : null);
2105
+ const variant = this._resolvedVariant();
2139
2106
  return variant ? `${base} (${variant.label})` : base;
2140
2107
  },
2141
2108
  // Intent glyph, not row state - see switch-branch above.
@@ -2438,10 +2405,7 @@ export class AssistantSessionsPanel extends Widget {
2438
2405
  command: this._cmd('open-branch'),
2439
2406
  args: {
2440
2407
  session_id: b.session_id,
2441
- label: this._branchMenuLabel(b),
2442
- // The label reads this to tell an attach from a launch; the
2443
- // provider's own worker capability is tested where it is answered.
2444
- ...(b.bg_id ? { bg: b.bg_id } : {})
2408
+ label: this._branchMenuLabel(b)
2445
2409
  }
2446
2410
  });
2447
2411
  }
@@ -2465,7 +2429,7 @@ export class AssistantSessionsPanel extends Widget {
2465
2429
  branchName: b => this._branchDisplayName(b),
2466
2430
  formatTime: ms => this._formatRelativeTime(ms),
2467
2431
  branchBadge: b => this._bgBadgeFor(b),
2468
- openTitle: id => this._openBranchTitle(session, id),
2432
+ openTitle: () => this._openBranchTitle(session),
2469
2433
  onSwitch: id => void this._switchBranch(session, id),
2470
2434
  onOpen: id => void this._openSession(session, undefined, id),
2471
2435
  onDelete: ids => this._deleteBranches(session, ids),
@@ -2477,21 +2441,16 @@ export class AssistantSessionsPanel extends Widget {
2477
2441
  }
2478
2442
  /** Tooltip for one Open button in the Manage Sessions popup. Same rule as the
2479
2443
  * Open Branched Conversation submenu: the button launches a terminal, so it
2480
- * names the mode that launch carries, and a conversation a live worker holds
2481
- * is attached to and carries none. The row's own conversation is not in
2482
- * `_lastBranches`, so it answers from the session. */
2483
- _openBranchTitle(session, sessionId) {
2484
- var _a, _b, _c, _d;
2444
+ * names the mode that launch carries. */
2445
+ _openBranchTitle(session) {
2446
+ var _a, _b, _c;
2485
2447
  // One terminal serves a project-scoped assistant, so the button focuses
2486
2448
  // or starts it, under the provider's own verb for that.
2487
2449
  if (this._descriptor.terminalScope === 'project') {
2488
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");
2489
2451
  }
2490
2452
  const base = 'Open this conversation in its own terminal';
2491
- const target = sessionId === session.session_id
2492
- ? session
2493
- : ((_d = this._lastBranches.find(b => b.session_id === sessionId)) !== null && _d !== void 0 ? _d : null);
2494
- const variant = this._resumeVariant(target);
2453
+ const variant = this._resolvedVariant();
2495
2454
  return variant ? `${base} (${variant.label})` : base;
2496
2455
  }
2497
2456
  /** The live-worker chip for a branch row, when the provider has workers at
@@ -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[];
@@ -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.31",
3
+ "version": "1.2.38",
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",
@@ -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');
@@ -1927,10 +1927,8 @@ export class AssistantSessionsPanel extends Widget {
1927
1927
  }
1928
1928
 
1929
1929
  // No star in the Favorites section - every row there is one by definition.
1930
- // The star sits before the time label and beside it: the label sizes to its
1931
- // own text, so the distance between the two is the row's gap on every row
1932
- // rather than however much shorter than "11mo ago" this label happens to
1933
- // be.
1930
+ // The star sits before the time so the minimum-width time column stays the
1931
+ // rightmost alignment anchor across all rows.
1934
1932
  if (session.favourite && sectionKey !== 'favourites') {
1935
1933
  // A marker, not a control. It carried a `title` that made it read as
1936
1934
  // one while a click fell through to the row and launched a terminal;
@@ -1944,8 +1942,8 @@ export class AssistantSessionsPanel extends Widget {
1944
1942
  row.appendChild(star);
1945
1943
  }
1946
1944
 
1947
- // Always present, empty when the session carries no mtime, so every row
1948
- // has the same elements in the same order whatever the data holds.
1945
+ // Always present (empty without an mtime) so the star column keeps the
1946
+ // same anchor across every row in the panel.
1949
1947
  const time = document.createElement('span');
1950
1948
  time.className = 'jp-AiAssistantsPanel-rowTime';
1951
1949
  time.textContent = session.file_mtime
@@ -2088,15 +2086,12 @@ export class AssistantSessionsPanel extends Widget {
2088
2086
  lines.push('Status: active');
2089
2087
  }
2090
2088
  if (this._descriptor.hasBgAgents && s.bg_id) {
2091
- 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
+ );
2092
2092
  }
2093
2093
  lines.push(...(this._hooks.tooltipLines?.(s) ?? []));
2094
- // What THIS row's click resolves to, which is the attach-aware question:
2095
- // the line above may have just said the click joins a live worker, and an
2096
- // attach is issued before the mode is appended, so `_resolvedVariant`
2097
- // promised a flag the server discards - one tooltip naming both, a line
2098
- // apart (DEF-46).
2099
- const variant = this._resumeVariant(s);
2094
+ const variant = this._resolvedVariant();
2100
2095
  if (variant) {
2101
2096
  // The row click launches with this, and the menu is the only other place
2102
2097
  // that says so.
@@ -2232,30 +2227,6 @@ export class AssistantSessionsPanel extends Widget {
2232
2227
  return resolved;
2233
2228
  }
2234
2229
 
2235
- /** The same mode as it applies to RESUME - of the active row by default, or
2236
- * of whichever conversation is named.
2237
- *
2238
- * An attach is not a launch: the server returns the argv before the flag is
2239
- * appended, deliberately, so promising the mode on a conversation a live
2240
- * worker already holds would name something the assistant never receives.
2241
- * That is true of resuming and of nothing else, so it is tested HERE rather
2242
- * than in `_resolvedVariant` - which is read by the `+` button and both
2243
- * new-session and branch items, none of which attach. Testing it there let
2244
- * one right-click on a background-agent row strip the mode's name off every
2245
- * one of those surfaces panel-wide, while their launches went on carrying it
2246
- * (DEF-37).
2247
- *
2248
- * The argument is what lets a surface that describes SOMEONE ELSE'S resume -
2249
- * a row tooltip built during a render, a branch item in the submenu - ask the
2250
- * question about that conversation rather than about the active row.
2251
- */
2252
- private _resumeVariant(
2253
- target: { bg_id?: string | null } | null = this._activeSession
2254
- ): IResolvedLaunchMode | null {
2255
- const joining = this._descriptor.hasBgAgents && !!target?.bg_id;
2256
- return joining ? null : this._resolvedVariant();
2257
- }
2258
-
2259
2230
  /** The variant suffix a plain item wears while that mode is in force. */
2260
2231
  private _variantSuffix(): string {
2261
2232
  const resolved = this._resolvedVariant();
@@ -2304,15 +2275,14 @@ export class AssistantSessionsPanel extends Widget {
2304
2275
  });
2305
2276
 
2306
2277
  this._commands.addCommand(this._cmd('resume'), {
2307
- // Joining a live worker and resuming a dormant conversation are
2308
- // different verbs, so a provider that has both says which one this row
2309
- // 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.
2310
2280
  label: () => {
2311
- const variant = this._resumeVariant();
2281
+ const variant = this._resolvedVariant();
2312
2282
  const verb = this._hooks.resumeLabel?.(active()) ?? 'Resume';
2313
2283
  return variant ? `${verb} (${variant.label})` : verb;
2314
2284
  },
2315
- icon: () => (this._resumeVariant()?.unsafe ? shieldIcon : undefined),
2285
+ icon: () => (this._resolvedVariant()?.unsafe ? shieldIcon : undefined),
2316
2286
  execute: () => {
2317
2287
  const s = active();
2318
2288
  if (s) {
@@ -2323,15 +2293,14 @@ export class AssistantSessionsPanel extends Widget {
2323
2293
 
2324
2294
  for (const mode of this._variantModes) {
2325
2295
  this._commands.addCommand(this._cmd(`resume-${mode.id}`), {
2326
- 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})`,
2327
2300
  icon: shieldIcon,
2328
2301
  // Absent while the plain item already resolves to this same mode - it
2329
2302
  // would be a second entry building an identical launch.
2330
2303
  isVisible: () => !this._buildsSameLaunch(mode),
2331
- // A conversation a live worker already holds was started in whatever
2332
- // mode that worker has; joining it cannot change that, so the variant
2333
- // is disabled rather than silently joining in the wrong mode.
2334
- isEnabled: () => !(this._descriptor.hasBgAgents && active()?.bg_id),
2335
2304
  execute: () => {
2336
2305
  const s = active();
2337
2306
  if (s) {
@@ -2504,14 +2473,10 @@ export class AssistantSessionsPanel extends Widget {
2504
2473
  this._commands.addCommand(this._cmd('open-branch'), {
2505
2474
  // Names the mode the launch carries, like Resume, `+` and Branch Session
2506
2475
  // do - this item opens a terminal, so the mode in force applies to it and
2507
- // this was the one launch surface that stayed silent about it. Attach-
2508
- // aware per conversation: the submenu passes `bg`, since a branch a live
2509
- // 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.
2510
2477
  label: args => {
2511
2478
  const base = String(args.label ?? '');
2512
- const variant = this._resumeVariant(
2513
- args.bg ? { bg_id: String(args.bg) } : null
2514
- );
2479
+ const variant = this._resolvedVariant();
2515
2480
  return variant ? `${base} (${variant.label})` : base;
2516
2481
  },
2517
2482
  // Intent glyph, not row state - see switch-branch above.
@@ -2846,10 +2811,7 @@ export class AssistantSessionsPanel extends Widget {
2846
2811
  command: this._cmd('open-branch'),
2847
2812
  args: {
2848
2813
  session_id: b.session_id,
2849
- label: this._branchMenuLabel(b),
2850
- // The label reads this to tell an attach from a launch; the
2851
- // provider's own worker capability is tested where it is answered.
2852
- ...(b.bg_id ? { bg: b.bg_id } : {})
2814
+ label: this._branchMenuLabel(b)
2853
2815
  }
2854
2816
  });
2855
2817
  }
@@ -2877,7 +2839,7 @@ export class AssistantSessionsPanel extends Widget {
2877
2839
  branchName: b => this._branchDisplayName(b),
2878
2840
  formatTime: ms => this._formatRelativeTime(ms),
2879
2841
  branchBadge: b => this._bgBadgeFor(b),
2880
- openTitle: id => this._openBranchTitle(session, id),
2842
+ openTitle: () => this._openBranchTitle(session),
2881
2843
  onSwitch: id => void this._switchBranch(session, id),
2882
2844
  onOpen: id => void this._openSession(session, undefined, id),
2883
2845
  onDelete: ids => this._deleteBranches(session, ids),
@@ -2890,10 +2852,8 @@ export class AssistantSessionsPanel extends Widget {
2890
2852
 
2891
2853
  /** Tooltip for one Open button in the Manage Sessions popup. Same rule as the
2892
2854
  * Open Branched Conversation submenu: the button launches a terminal, so it
2893
- * names the mode that launch carries, and a conversation a live worker holds
2894
- * is attached to and carries none. The row's own conversation is not in
2895
- * `_lastBranches`, so it answers from the session. */
2896
- private _openBranchTitle(session: ISession, sessionId: string): string {
2855
+ * names the mode that launch carries. */
2856
+ private _openBranchTitle(session: ISession): string {
2897
2857
  // One terminal serves a project-scoped assistant, so the button focuses
2898
2858
  // or starts it, under the provider's own verb for that.
2899
2859
  if (this._descriptor.terminalScope === 'project') {
@@ -2902,11 +2862,7 @@ export class AssistantSessionsPanel extends Widget {
2902
2862
  );
2903
2863
  }
2904
2864
  const base = 'Open this conversation in its own terminal';
2905
- const target =
2906
- sessionId === session.session_id
2907
- ? session
2908
- : (this._lastBranches.find(b => b.session_id === sessionId) ?? null);
2909
- const variant = this._resumeVariant(target);
2865
+ const variant = this._resolvedVariant();
2910
2866
  return variant ? `${base} (${variant.label})` : base;
2911
2867
  }
2912
2868
 
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[];
@@ -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
  };
package/style/base.css CHANGED
@@ -13,6 +13,14 @@
13
13
  on purpose - the partial row is the scroll-continuation cue. */
14
14
  --aica-row-height: 24px;
15
15
 
16
+ /* Single source for the gap between the panel border and its content, on
17
+ both sides: the header, search field, error line, section headers and rows
18
+ all read it, so the title, the search field, the caret column and the
19
+ status-dot column keep one left edge. 4px, not 8px: the lists reserve a
20
+ scrollbar gutter as well, so 8px put the time column 23px from the right
21
+ border where the file browser's sits 12px from it. */
22
+ --aica-inset: 4px;
23
+
16
24
  display: flex;
17
25
  flex-direction: column;
18
26
  position: relative;
@@ -27,7 +35,7 @@
27
35
  .jp-AiAssistantsPanel-header {
28
36
  display: flex;
29
37
  align-items: center;
30
- padding: 4px 8px;
38
+ padding: 4px var(--aica-inset);
31
39
  border-bottom: 1px solid var(--jp-border-color2);
32
40
  background: var(--jp-layout-color2);
33
41
  flex: 0 0 auto;
@@ -87,7 +95,7 @@
87
95
  flex: 0 0 auto;
88
96
  display: flex;
89
97
  position: relative;
90
- margin: 4px 8px;
98
+ margin: 4px var(--aica-inset);
91
99
  }
92
100
 
93
101
  /* `display: flex` above would beat the user-agent `[hidden] { display: none }`,
@@ -169,7 +177,7 @@
169
177
  server must leave the other panels alone. */
170
178
  .jp-AiAssistantsPanel-error {
171
179
  flex: 0 0 auto;
172
- margin: 4px 8px;
180
+ margin: 4px var(--aica-inset);
173
181
  padding: 4px 6px;
174
182
  border-radius: 2px;
175
183
 
@@ -246,7 +254,7 @@
246
254
  display: flex;
247
255
  align-items: center;
248
256
  width: 100%;
249
- padding: 4px 8px;
257
+ padding: 4px var(--aica-inset);
250
258
  background: var(--jp-layout-color2);
251
259
  border: none;
252
260
  cursor: pointer;
@@ -305,7 +313,7 @@
305
313
  .jp-AiAssistantsPanel-row {
306
314
  display: flex;
307
315
  align-items: center;
308
- padding: 0 8px;
316
+ padding: 0 var(--aica-inset);
309
317
  height: var(--aica-row-height);
310
318
  line-height: var(--aica-row-height);
311
319
  cursor: pointer;
@@ -956,20 +964,21 @@ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotGlyph {
956
964
  color: var(--jp-layout-color1);
957
965
  }
958
966
 
959
- /* Last-activity time on session rows - dim, and ending on the same vertical
960
- line down the whole panel because it is the last item on a row whose name
961
- takes all the slack.
962
- The label sizes to its own text. A fixed 4.5em box held the difference
963
- between the label and the widest one ("11mo ago") open as empty space on its
964
- LEFT, which is where the favourite star sits, so the star stood 32px from a
965
- short label such as "now" and 6px from the longest - measured on the row
966
- markup against this stylesheet, 2026-09-23. Sizing to the text puts the star
967
- the row's own 6px gap from every label, and removes the one case the fixed
968
- box still got wrong: a label wider than the box overflowed it and broke the
969
- alignment the box was there to keep. Tabular figures keep the digits an even
970
- width, as the branch popup's twin already does. */
967
+ /* Last-activity time on session rows - dim, minimum-width right-aligned column so
968
+ values line up across rows; the star column sits before it. */
971
969
  .jp-AiAssistantsPanel-rowTime {
972
970
  flex: 0 0 auto;
971
+
972
+ /* 4.5em, not 4em: the longest label this column draws ("11mo ago") measures
973
+ 47.4px against a 43.3px box, so the one column promised as fixed-width
974
+ right-aligned went ragged on exactly the oldest rows. A minimum, not a
975
+ width: where system-ui is a wider face - DejaVu Sans on a stock Linux
976
+ desktop, 54.5px for the same label - a fixed box let the label spill 5.8px
977
+ past the column's right edge; a minimum grows that one box leftward
978
+ instead, so the labels stay aligned and only that row's star moves.
979
+ Tabular figures keep the digits aligned down the column, as the branch
980
+ popup's twin already does. */
981
+ min-width: 4.5em;
973
982
  white-space: nowrap;
974
983
  text-align: right;
975
984
  color: var(--jp-ui-font-color2);