jupyterlab_ai_code_assistants_extension 1.2.118 → 1.2.123

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
@@ -33,7 +33,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
33
33
  - **File browser follows the opened session** - opening a session moves the JupyterLab file browser to that session's project folder (a setting, on by default); a right-click on a panel header toggles this and moves the panels between the left and right sidebars
34
34
  - **Launcher tiles** - every enabled assistant has a tile in an "AI Assistants" section of the JupyterLab Launcher (after Other, under the extension's own robot-head icon); a click opens the assistant in the file browser's current folder, resuming the folder's conversation when one exists and starting a new one otherwise, through the companion `jupyterlab_basic_terminal_extension` (installed automatically). Disable an assistant and its tile goes; disable them all and the section goes with them
35
35
  - **Your terminal's `claude -c` follows the panel** (Claude) - switching to a conversation makes plain `claude -c` in that project resume that same conversation, including one that has been compacted, which Claude Code otherwise refuses to continue. Launching repairs a compacted conversation the same way, but only switching moves what `claude -c` picks
36
- - **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id with last-activity time; "Manage Sessions..." opens a searchable popup over the full list with multi-select delete and per-row open and copy-id buttons
36
+ - **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id with last-activity time; "Manage Sessions..." opens a searchable popup over the full list with multi-select delete, a per-row action that switches or opens according to the submenu it was opened from, and a copy-id button
37
37
  - **Branch session** - fork the current conversation into a new named session via the right-click menu; each assistant forks its own way (Claude's native `--fork-session`, Codex's app-server `thread/fork`, server-side copies for Kimi, Gemini and DeepSeek) behind the same menu item
38
38
  - **Rename session** - name a conversation from the right-click menu, and the name is written where the assistant itself reads it back: Claude gets the record its own `/rename` writes, Kimi a custom title, Gemini the chat's summary, DeepSeek its title event. The panel shows the name the assistant actually kept. Codex's name is set through Codex's own app-server, so its CLI lists the same name the panel shows
39
39
  - **Launch modes under each assistant's own name** - Claude's skip-permissions, Codex's approval bypass, Kimi's `--yolo`, Gemini's YOLO; unsafe variants carry a shield glyph in the launch menus
package/lib/core/panel.js CHANGED
@@ -2083,7 +2083,13 @@ export class AssistantSessionsPanel extends Widget {
2083
2083
  });
2084
2084
  this._commands.addCommand(this._cmd('manage-sessions'), {
2085
2085
  label: () => `Manage Sessions... (${this._lastBranches.length})`,
2086
- execute: () => this._openManagePopup()
2086
+ execute: () => this._openManagePopup('switch')
2087
+ });
2088
+ // The open submenu's copy of the item: same popup, but a row action
2089
+ // launches the conversation rather than making it current.
2090
+ this._commands.addCommand(this._cmd('manage-sessions-open'), {
2091
+ label: () => `Manage Sessions... (${this._lastBranches.length})`,
2092
+ execute: () => this._openManagePopup('open')
2087
2093
  });
2088
2094
  this._commands.addCommand(this._cmd('open-branch'), {
2089
2095
  // Names the mode the launch carries, like Resume, `+` and Branch Session
@@ -2417,10 +2423,10 @@ export class AssistantSessionsPanel extends Widget {
2417
2423
  }
2418
2424
  this._openBranchSubmenu.addItem({ type: 'separator' });
2419
2425
  this._openBranchSubmenu.addItem({
2420
- command: this._cmd('manage-sessions')
2426
+ command: this._cmd('manage-sessions-open')
2421
2427
  });
2422
2428
  }
2423
- _openManagePopup() {
2429
+ _openManagePopup(mode) {
2424
2430
  const session = this._activeSession;
2425
2431
  if (!session) {
2426
2432
  return;
@@ -2428,6 +2434,7 @@ export class AssistantSessionsPanel extends Widget {
2428
2434
  const current = this._lastBranchesCurrent || session.session_id;
2429
2435
  showManageSessionsPopup({
2430
2436
  trans: this._trans,
2437
+ mode,
2431
2438
  branches: this._lastBranches,
2432
2439
  current,
2433
2440
  currentName: `${this._lookupName(session)} (${shortSessionId(current, this._descriptor.sessionIdPrefix)})`,
@@ -29,6 +29,10 @@ export declare namespace ManageSessionsPopup {
29
29
  formatTime: (epochMs: number) => string;
30
30
  /** Provider chip for a branch row, or null. */
31
31
  branchBadge?: (branch: IBranch) => HTMLElement | null;
32
+ /** What a row's action does, inherited from the submenu that opened the
33
+ * popup: `switch` makes the conversation current, `open` launches it in
34
+ * its own terminal. */
35
+ mode: 'switch' | 'open';
32
36
  /** Tooltip for one row's Open button. The panel owns it because the launch
33
37
  * mode in force is panel state, and every other surface that opens a
34
38
  * conversation names it. */
package/lib/core/popup.js CHANGED
@@ -115,19 +115,32 @@ export function showManageSessionsPopup(options) {
115
115
  });
116
116
  // Scopes the action-row layout in base.css to this one dialog.
117
117
  dialog.addClass('jp-AiAssistantsPanel-branchDialog');
118
- // Per-row "Open" launches that conversation in its own terminal and closes
119
- // the popup. stopPropagation keeps the click from toggling selection or
120
- // switching the row.
121
- const openButton = (sessionId) => {
118
+ // The row action follows the mode the invoking submenu passed: switch makes
119
+ // the conversation current, open launches it in its own terminal. Both close
120
+ // the popup first - the action is complete once it runs.
121
+ const runAction = (sessionId) => {
122
+ dialog.dispose();
123
+ if (options.mode === 'switch') {
124
+ options.onSwitch(sessionId);
125
+ }
126
+ else {
127
+ options.onOpen(sessionId);
128
+ }
129
+ };
130
+ // Per-row action button. stopPropagation keeps the click from toggling
131
+ // selection or repeating the action through the row's own click handler.
132
+ const actionButton = (sessionId) => {
122
133
  const btn = document.createElement('button');
123
134
  btn.type = 'button';
124
135
  btn.className = 'jp-AiAssistantsPanel-branchOpen';
125
- btn.textContent = 'Open';
126
- btn.title = options.openTitle();
136
+ btn.textContent = options.mode === 'switch' ? 'Switch' : 'Open';
137
+ btn.title =
138
+ options.mode === 'switch'
139
+ ? "Make this the project's current conversation"
140
+ : options.openTitle();
127
141
  btn.addEventListener('click', e => {
128
142
  e.stopPropagation();
129
- dialog.dispose();
130
- options.onOpen(sessionId);
143
+ runAction(sessionId);
131
144
  });
132
145
  return btn;
133
146
  };
@@ -192,7 +205,11 @@ export function showManageSessionsPopup(options) {
192
205
  badge.className = 'jp-AiAssistantsPanel-branchCurrentBadge';
193
206
  badge.textContent = 'current';
194
207
  currentRow.appendChild(badge);
195
- currentRow.appendChild(openButton(options.current));
208
+ // Switch mode has nothing to switch to on the row that is already current;
209
+ // open mode can still launch it in a terminal.
210
+ if (options.mode === 'open') {
211
+ currentRow.appendChild(actionButton(options.current));
212
+ }
196
213
  currentRow.appendChild(copyButton(options.current, trans));
197
214
  list.appendChild(currentRow);
198
215
  const matches = visibleMatches();
@@ -270,20 +287,19 @@ export function showManageSessionsPopup(options) {
270
287
  time.className = 'jp-AiAssistantsPanel-branchTime';
271
288
  time.textContent = options.formatTime(b.file_mtime);
272
289
  row.appendChild(time);
273
- row.appendChild(openButton(b.session_id));
290
+ row.appendChild(actionButton(b.session_id));
274
291
  row.appendChild(copyButton(b.session_id, trans));
275
- // A row click always switches, whatever is selected: the checkbox cell
276
- // is the one select target, so a click has one meaning (DEF-PANE-209).
277
- const activate = () => {
278
- dialog.dispose();
279
- options.onSwitch(b.session_id);
280
- };
292
+ // A row click follows the popup's mode, whatever is selected: the
293
+ // checkbox cell is the one select target, so a click has one meaning
294
+ // (DEF-PANE-209).
295
+ const activate = () => runAction(b.session_id);
281
296
  row.addEventListener('click', activate);
282
- // Switch has no button of its own, so it needs a role and a key route or
283
- // it is reachable by mouse only. The affordance sits on the label span,
284
- // not the row: the row also hosts a checkbox and the Open/Copy buttons,
285
- // and a role="button" wrapping them both swallows their own Enter/Space
286
- // and makes them presentational children (ARIA nested-interactive).
297
+ // The row action also has a button; the label keeps a role and key route
298
+ // so the row itself stays keyboard-reachable. The affordance sits on the
299
+ // label span, not the row: the row also hosts a checkbox and the
300
+ // action/Copy buttons, and a role="button" wrapping them both swallows
301
+ // their own Enter/Space and makes them presentational children (ARIA
302
+ // nested-interactive).
287
303
  label.setAttribute('role', 'button');
288
304
  label.tabIndex = 0;
289
305
  label.addEventListener('keydown', e => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_ai_code_assistants_extension",
3
- "version": "1.2.118",
3
+ "version": "1.2.123",
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",
@@ -986,17 +986,43 @@ describe('the Manage Sessions popup is handed the current row name', () => {
986
986
  name: 'proj',
987
987
  session_id: 'session_9f8e7d6c-1111-2222-3333-444455556666'
988
988
  });
989
- (p as any)._openManagePopup();
989
+ (p as any)._openManagePopup('switch');
990
990
  const calls = (showManageSessionsPopup as jest.Mock).mock.calls;
991
991
  const [options] = calls[calls.length - 1];
992
992
  // A front-slice would read `proj (session_)` on every conversation.
993
993
  expect(options.currentName).toEqual('proj (9f8e7d6c)');
994
+ expect(options.mode).toEqual('switch');
994
995
  } finally {
995
996
  p.dispose();
996
997
  }
997
998
  });
998
999
  });
999
1000
 
1001
+ describe('Manage Sessions popup mode follows the invoking submenu (DEF-PANE-326)', () => {
1002
+ const lastOptions = (): any => {
1003
+ const calls = (showManageSessionsPopup as jest.Mock).mock.calls;
1004
+ return calls[calls.length - 1][0];
1005
+ };
1006
+
1007
+ it('the switch submenu opens the popup in switch mode', () => {
1008
+ (panel as any)._activeSession = session({ name: 'proj' });
1009
+ (panel as any)._fillBranchSubmenus([]);
1010
+ const item = (panel as any)._switchSubmenu.items.at(-1);
1011
+ (panel as any)._commands.execute(item.command, item.args);
1012
+
1013
+ expect(lastOptions().mode).toBe('switch');
1014
+ });
1015
+
1016
+ it('the open submenu opens the popup in open mode', () => {
1017
+ (panel as any)._activeSession = session({ name: 'proj' });
1018
+ (panel as any)._fillBranchSubmenus([]);
1019
+ const item = (panel as any)._openBranchSubmenu.items.at(-1);
1020
+ (panel as any)._commands.execute(item.command, item.args);
1021
+
1022
+ expect(lastOptions().mode).toBe('open');
1023
+ });
1024
+ });
1025
+
1000
1026
  describe('per-conversation open surfaces', () => {
1001
1027
  function submenuLabels(p: AssistantSessionsPanel): string[] {
1002
1028
  (p as any)._rebuildContextMenu(true);
@@ -1827,7 +1853,7 @@ describe('ACC-SESS-190..192 - opening a session moves the file browser', () => {
1827
1853
  'the Manage Sessions Open button',
1828
1854
  (p, row) => {
1829
1855
  (p as any)._activeSession = row;
1830
- (p as any)._openManagePopup();
1856
+ (p as any)._openManagePopup('open');
1831
1857
  const calls = (showManageSessionsPopup as jest.Mock).mock.calls;
1832
1858
  calls[calls.length - 1][0].onOpen('sid-branch');
1833
1859
  }
@@ -26,13 +26,15 @@ function branch(sessionId: string, label: string): IBranch {
26
26
  }
27
27
 
28
28
  let onSwitch: jest.Mock;
29
+ let onOpen: jest.Mock;
29
30
  let onDelete: jest.Mock;
30
31
  // Each test opens a fresh popup in beforeEach; a describe that needs the
31
32
  // permanent mode flips this in its beforeAll, which runs ahead of it.
32
33
  let deleteToTrash = true;
33
34
 
34
- function open(): void {
35
+ function open(mode: 'switch' | 'open' = 'switch'): void {
35
36
  showManageSessionsPopup({
37
+ mode,
36
38
  branches: [branch(FIRST, 'first'), branch(SECOND, 'second')],
37
39
  current: 'session-00000000-0000-4000-8000-000000000000',
38
40
  currentName: 'proj (00000000)',
@@ -41,12 +43,20 @@ function open(): void {
41
43
  formatTime: () => 'now',
42
44
  openTitle: () => 'Open',
43
45
  onSwitch,
44
- onOpen: jest.fn(),
46
+ onOpen,
45
47
  onDelete,
46
48
  onRefetch: jest.fn(async () => [])
47
49
  });
48
50
  }
49
51
 
52
+ /** Replace the default switch-mode popup with one opened in `mode`, and let
53
+ * the launch queue attach it before the test queries rows. */
54
+ async function reopen(mode: 'switch' | 'open'): Promise<void> {
55
+ Dialog.flush();
56
+ open(mode);
57
+ await new Promise(resolve => setTimeout(resolve, 0));
58
+ }
59
+
50
60
  function rows(): HTMLElement[] {
51
61
  return Array.from(
52
62
  document.querySelectorAll<HTMLElement>('.jp-AiAssistantsPanel-branchRow')
@@ -63,8 +73,16 @@ function deleteButton(): HTMLButtonElement {
63
73
  )!;
64
74
  }
65
75
 
76
+ /** The per-row action button - Switch or Open, by popup mode. */
77
+ function actionButtons(row: HTMLElement): HTMLButtonElement[] {
78
+ return Array.from(
79
+ row.querySelectorAll<HTMLButtonElement>('.jp-AiAssistantsPanel-branchOpen')
80
+ );
81
+ }
82
+
66
83
  beforeEach(() => {
67
84
  onSwitch = jest.fn();
85
+ onOpen = jest.fn();
68
86
  onDelete = jest.fn(async () => 0);
69
87
  open();
70
88
  });
@@ -100,6 +118,61 @@ describe('Manage Sessions popup click zones', () => {
100
118
  });
101
119
  });
102
120
 
121
+ describe('Manage Sessions popup action mode (DEF-PANE-326)', () => {
122
+ it('switch mode labels non-current rows Switch and calls onSwitch', () => {
123
+ const [, first, second] = rows();
124
+ const buttons = [...actionButtons(first), ...actionButtons(second)];
125
+ expect(buttons.map(b => b.textContent)).toEqual(['Switch', 'Switch']);
126
+ expect(buttons[0].title).toBe(
127
+ "Make this the project's current conversation"
128
+ );
129
+
130
+ actionButtons(second)[0].click();
131
+
132
+ expect(onSwitch).toHaveBeenCalledWith(SECOND);
133
+ expect(onOpen).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it('open mode labels non-current rows Open and calls onOpen', async () => {
137
+ await reopen('open');
138
+ const [, first, second] = rows();
139
+ const buttons = [...actionButtons(first), ...actionButtons(second)];
140
+ expect(buttons.map(b => b.textContent)).toEqual(['Open', 'Open']);
141
+ expect(buttons[0].title).toBe('Open');
142
+
143
+ actionButtons(second)[0].click();
144
+
145
+ expect(onOpen).toHaveBeenCalledWith(SECOND);
146
+ expect(onSwitch).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it('a row click switches in switch mode and opens in open mode', async () => {
150
+ const [, first] = rows();
151
+ first.click();
152
+
153
+ expect(onSwitch).toHaveBeenCalledWith(FIRST);
154
+ expect(onOpen).not.toHaveBeenCalled();
155
+
156
+ await reopen('open');
157
+ const [, openFirst] = rows();
158
+ openFirst.click();
159
+
160
+ expect(onOpen).toHaveBeenCalledWith(FIRST);
161
+ expect(onSwitch).toHaveBeenCalledTimes(1);
162
+ });
163
+
164
+ it('the current row has no action in switch mode and Open in open mode', async () => {
165
+ const [current] = rows();
166
+ expect(actionButtons(current)).toHaveLength(0);
167
+
168
+ await reopen('open');
169
+ const [openCurrent] = rows();
170
+ expect(actionButtons(openCurrent).map(b => b.textContent)).toEqual([
171
+ 'Open'
172
+ ]);
173
+ });
174
+ });
175
+
103
176
  describe('Manage Sessions popup focus (DEF-PANE-314)', () => {
104
177
  // The popup moves the focus to the filter once the dialog is ready.
105
178
  const ready = (): Promise<void> => Dialog.tracker.find(() => true)!.ready;
package/src/core/panel.ts CHANGED
@@ -2424,7 +2424,14 @@ export class AssistantSessionsPanel extends Widget {
2424
2424
 
2425
2425
  this._commands.addCommand(this._cmd('manage-sessions'), {
2426
2426
  label: () => `Manage Sessions... (${this._lastBranches.length})`,
2427
- execute: () => this._openManagePopup()
2427
+ execute: () => this._openManagePopup('switch')
2428
+ });
2429
+
2430
+ // The open submenu's copy of the item: same popup, but a row action
2431
+ // launches the conversation rather than making it current.
2432
+ this._commands.addCommand(this._cmd('manage-sessions-open'), {
2433
+ label: () => `Manage Sessions... (${this._lastBranches.length})`,
2434
+ execute: () => this._openManagePopup('open')
2428
2435
  });
2429
2436
 
2430
2437
  this._commands.addCommand(this._cmd('open-branch'), {
@@ -2791,11 +2798,11 @@ export class AssistantSessionsPanel extends Widget {
2791
2798
  }
2792
2799
  this._openBranchSubmenu.addItem({ type: 'separator' });
2793
2800
  this._openBranchSubmenu.addItem({
2794
- command: this._cmd('manage-sessions')
2801
+ command: this._cmd('manage-sessions-open')
2795
2802
  });
2796
2803
  }
2797
2804
 
2798
- private _openManagePopup(): void {
2805
+ private _openManagePopup(mode: 'switch' | 'open'): void {
2799
2806
  const session = this._activeSession;
2800
2807
  if (!session) {
2801
2808
  return;
@@ -2803,6 +2810,7 @@ export class AssistantSessionsPanel extends Widget {
2803
2810
  const current = this._lastBranchesCurrent || session.session_id;
2804
2811
  showManageSessionsPopup({
2805
2812
  trans: this._trans,
2813
+ mode,
2806
2814
  branches: this._lastBranches,
2807
2815
  current,
2808
2816
  currentName: `${this._lookupName(session)} (${shortSessionId(
package/src/core/popup.ts CHANGED
@@ -35,6 +35,10 @@ export namespace ManageSessionsPopup {
35
35
  formatTime: (epochMs: number) => string;
36
36
  /** Provider chip for a branch row, or null. */
37
37
  branchBadge?: (branch: IBranch) => HTMLElement | null;
38
+ /** What a row's action does, inherited from the submenu that opened the
39
+ * popup: `switch` makes the conversation current, `open` launches it in
40
+ * its own terminal. */
41
+ mode: 'switch' | 'open';
38
42
  /** Tooltip for one row's Open button. The panel owns it because the launch
39
43
  * mode in force is panel state, and every other surface that opens a
40
44
  * conversation names it. */
@@ -185,19 +189,32 @@ export function showManageSessionsPopup(
185
189
  // Scopes the action-row layout in base.css to this one dialog.
186
190
  dialog.addClass('jp-AiAssistantsPanel-branchDialog');
187
191
 
188
- // Per-row "Open" launches that conversation in its own terminal and closes
189
- // the popup. stopPropagation keeps the click from toggling selection or
190
- // switching the row.
191
- const openButton = (sessionId: string): HTMLButtonElement => {
192
+ // The row action follows the mode the invoking submenu passed: switch makes
193
+ // the conversation current, open launches it in its own terminal. Both close
194
+ // the popup first - the action is complete once it runs.
195
+ const runAction = (sessionId: string): void => {
196
+ dialog.dispose();
197
+ if (options.mode === 'switch') {
198
+ options.onSwitch(sessionId);
199
+ } else {
200
+ options.onOpen(sessionId);
201
+ }
202
+ };
203
+
204
+ // Per-row action button. stopPropagation keeps the click from toggling
205
+ // selection or repeating the action through the row's own click handler.
206
+ const actionButton = (sessionId: string): HTMLButtonElement => {
192
207
  const btn = document.createElement('button');
193
208
  btn.type = 'button';
194
209
  btn.className = 'jp-AiAssistantsPanel-branchOpen';
195
- btn.textContent = 'Open';
196
- btn.title = options.openTitle();
210
+ btn.textContent = options.mode === 'switch' ? 'Switch' : 'Open';
211
+ btn.title =
212
+ options.mode === 'switch'
213
+ ? "Make this the project's current conversation"
214
+ : options.openTitle();
197
215
  btn.addEventListener('click', e => {
198
216
  e.stopPropagation();
199
- dialog.dispose();
200
- options.onOpen(sessionId);
217
+ runAction(sessionId);
201
218
  });
202
219
  return btn;
203
220
  };
@@ -271,7 +288,11 @@ export function showManageSessionsPopup(
271
288
  badge.className = 'jp-AiAssistantsPanel-branchCurrentBadge';
272
289
  badge.textContent = 'current';
273
290
  currentRow.appendChild(badge);
274
- currentRow.appendChild(openButton(options.current));
291
+ // Switch mode has nothing to switch to on the row that is already current;
292
+ // open mode can still launch it in a terminal.
293
+ if (options.mode === 'open') {
294
+ currentRow.appendChild(actionButton(options.current));
295
+ }
275
296
  currentRow.appendChild(copyButton(options.current, trans));
276
297
  list.appendChild(currentRow);
277
298
 
@@ -353,21 +374,20 @@ export function showManageSessionsPopup(
353
374
  time.textContent = options.formatTime(b.file_mtime);
354
375
  row.appendChild(time);
355
376
 
356
- row.appendChild(openButton(b.session_id));
377
+ row.appendChild(actionButton(b.session_id));
357
378
  row.appendChild(copyButton(b.session_id, trans));
358
379
 
359
- // A row click always switches, whatever is selected: the checkbox cell
360
- // is the one select target, so a click has one meaning (DEF-PANE-209).
361
- const activate = (): void => {
362
- dialog.dispose();
363
- options.onSwitch(b.session_id);
364
- };
380
+ // A row click follows the popup's mode, whatever is selected: the
381
+ // checkbox cell is the one select target, so a click has one meaning
382
+ // (DEF-PANE-209).
383
+ const activate = (): void => runAction(b.session_id);
365
384
  row.addEventListener('click', activate);
366
- // Switch has no button of its own, so it needs a role and a key route or
367
- // it is reachable by mouse only. The affordance sits on the label span,
368
- // not the row: the row also hosts a checkbox and the Open/Copy buttons,
369
- // and a role="button" wrapping them both swallows their own Enter/Space
370
- // and makes them presentational children (ARIA nested-interactive).
385
+ // The row action also has a button; the label keeps a role and key route
386
+ // so the row itself stays keyboard-reachable. The affordance sits on the
387
+ // label span, not the row: the row also hosts a checkbox and the
388
+ // action/Copy buttons, and a role="button" wrapping them both swallows
389
+ // their own Enter/Space and makes them presentational children (ARIA
390
+ // nested-interactive).
371
391
  label.setAttribute('role', 'button');
372
392
  label.tabIndex = 0;
373
393
  label.addEventListener('keydown', e => {
package/style/base.css CHANGED
@@ -550,26 +550,6 @@ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotHalo {
550
550
  order: -1;
551
551
  }
552
552
 
553
- /* The spin carries "busy", not information, so under a reduced-motion
554
- preference it degrades to the same ring held still and dimmed. */
555
- @media (prefers-reduced-motion: reduce) {
556
- .jp-AiAssistantsPanel-iconButton.jp-mod-spinning svg,
557
- .jp-aiAssistants-spinner,
558
- .jp-AiAssistantsPanel-spinner {
559
- animation: none;
560
- opacity: 0.5;
561
- }
562
-
563
- /* The click-time busy spinner is the only thing on screen while a click is
564
- being handled, so it keeps full opacity: dimmed, its brand arc measures
565
- 1.74:1 on the light panel, below the 3:1 minimum for a non-text cue. Held
566
- still it would read as idle, so the overlay names the action in a visible
567
- label beside it; the label, not an animation, carries the busy state. */
568
- .jp-AiAssistants-busySpinner {
569
- opacity: 1;
570
- }
571
- }
572
-
573
553
  /* Full-panel veil shown only during an explicit refresh (refresh button or
574
554
  panel show) - the background poll never raises it. The rule is gated on
575
555
  :not([hidden]) because this author `display` would otherwise beat the UA