jupyterlab_claude_code_extension 1.1.21 → 1.1.25

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
@@ -19,8 +19,9 @@ Browse, resume, and manage your Claude Code sessions from a JupyterLab side pane
19
19
  - **One-click resume** - click a row to jump back into that session in a terminal. If a terminal for the project is already open, it's reused instead of duplicated
20
20
  - **Favorites** - star projects you keep coming back to via the right-click menu
21
21
  - **Remove** - drop a project's Claude history from the panel via the right-click menu; the history folder is moved to the trash (it honours JupyterLab's "move files to trash" setting), not deleted permanently
22
- - **Search** - fuzzy filter at the top of the panel
23
- - **Presentation modes** - label rows by session name, folder name, or path relative to the JupyterLab root
22
+ - **Clean up parallel sessions** - when a project has accumulated extra sessions beyond the main one, a right-click menu item (showing the count in brackets) removes them all, keeping only the main session; removed files honour the same trash setting
23
+ - **Search** - fuzzy filter toggled by the funnel button next to refresh
24
+ - **Presentation modes** - label rows by session name (so a `/rename` shows through), folder name, or path relative to the JupyterLab root
24
25
  - **Hover tooltip** with project path, last activity, message count, branch, and session id
25
26
  - **Auto-disabled** when the Claude Code CLI is not installed
26
27
 
package/lib/types.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface ISession {
13
13
  git_branch: string | null;
14
14
  remote_control: boolean;
15
15
  favourite: boolean;
16
+ extra_sessions: number;
16
17
  }
17
18
  export interface ISessionsListResponse {
18
19
  sessions: ISession[];
@@ -35,6 +36,12 @@ export interface IRemoveRequest {
35
36
  export interface IRemoveResponse {
36
37
  removed: string;
37
38
  }
39
+ export interface ICleanupRequest {
40
+ encoded_path: string;
41
+ }
42
+ export interface ICleanupResponse {
43
+ removed_count: number;
44
+ }
38
45
  export interface ILaunchTerminalRequest {
39
46
  project_path: string;
40
47
  session_id: string;
package/lib/widget.d.ts CHANGED
@@ -41,6 +41,7 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
41
41
  private _fetch;
42
42
  private _toggleFavourite;
43
43
  private _remove;
44
+ private _cleanupParallel;
44
45
  private _resumeInTerminal;
45
46
  private _doResumeInTerminal;
46
47
  /**
package/lib/widget.js CHANGED
@@ -334,6 +334,54 @@ export class ClaudeCodeSessionsWidget extends Widget {
334
334
  this._render();
335
335
  }
336
336
  }
337
+ async _cleanupParallel(session) {
338
+ const body = new Widget();
339
+ body.node.className = 'jp-ClaudeSessionsPanel-cleanupBody';
340
+ const message = document.createElement('div');
341
+ message.className = 'jp-ClaudeSessionsPanel-cleanupMessage';
342
+ const count = session.extra_sessions;
343
+ message.textContent = `Removing ${count} parallel session${count === 1 ? '' : 's'}...`;
344
+ body.node.appendChild(message);
345
+ // No `value` attribute -> indeterminate (animated) while the request is
346
+ // in flight; set to max on completion so the bar reads as finished.
347
+ const bar = document.createElement('progress');
348
+ bar.className = 'jp-ClaudeSessionsPanel-cleanupProgress';
349
+ bar.max = 1;
350
+ body.node.appendChild(bar);
351
+ const dialog = new Dialog({
352
+ title: 'Clean Up Parallel Sessions',
353
+ body,
354
+ buttons: [Dialog.okButton({ label: 'Close' })]
355
+ });
356
+ // Hide the Close button while work is in progress; restore it once the
357
+ // outcome (success or error) is shown so the user dismisses the popup.
358
+ const footer = dialog.node.querySelector('.jp-Dialog-footer');
359
+ if (footer) {
360
+ footer.style.display = 'none';
361
+ }
362
+ void dialog.launch();
363
+ try {
364
+ const data = await requestAPI('sessions/cleanup', this._serverSettings, {
365
+ method: 'POST',
366
+ body: JSON.stringify({ encoded_path: session.encoded_path })
367
+ });
368
+ bar.value = 1;
369
+ message.textContent = `Removed ${data.removed_count} parallel session${data.removed_count === 1 ? '' : 's'}.`;
370
+ // Refresh so the row's extra_sessions count (and menu label) update
371
+ await this._fetch();
372
+ }
373
+ catch (err) {
374
+ bar.remove();
375
+ message.classList.add('jp-ClaudeSessionsPanel-cleanupError');
376
+ message.textContent = `Cleanup failed: ${err instanceof Error ? err.message : String(err)}`;
377
+ this._showError(err);
378
+ }
379
+ finally {
380
+ if (footer) {
381
+ footer.style.display = '';
382
+ }
383
+ }
384
+ }
337
385
  // -------------------------------------------------------------- terminal
338
386
  async _resumeInTerminal(session, forceDangerous = false) {
339
387
  // Coalesce concurrent clicks on the same row - subsequent clicks attach
@@ -897,6 +945,15 @@ export class ClaudeCodeSessionsWidget extends Widget {
897
945
  Clipboard.copyToSystem(path);
898
946
  }
899
947
  });
948
+ this._commands.addCommand('claude-code-sessions:cleanup-parallel', {
949
+ label: () => { var _a, _b; return `Clean Up Parallel Sessions (${(_b = (_a = this._activeSession) === null || _a === void 0 ? void 0 : _a.extra_sessions) !== null && _b !== void 0 ? _b : 0})`; },
950
+ isVisible: () => { var _a, _b; return ((_b = (_a = this._activeSession) === null || _a === void 0 ? void 0 : _a.extra_sessions) !== null && _b !== void 0 ? _b : 0) > 0; },
951
+ execute: () => {
952
+ if (this._activeSession) {
953
+ void this._cleanupParallel(this._activeSession);
954
+ }
955
+ }
956
+ });
900
957
  this._commands.addCommand('claude-code-sessions:remove', {
901
958
  label: 'Remove from Claude',
902
959
  icon: removeIcon,
@@ -923,6 +980,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
923
980
  });
924
981
  this._contextMenu.addItem({ command: 'claude-code-sessions:copy-path' });
925
982
  this._contextMenu.addItem({ type: 'separator' });
983
+ this._contextMenu.addItem({
984
+ command: 'claude-code-sessions:cleanup-parallel'
985
+ });
926
986
  this._contextMenu.addItem({ command: 'claude-code-sessions:remove' });
927
987
  this._contextMenu.aboutToClose.connect(() => {
928
988
  // Only clear the visual highlight - DO NOT null _activeSession.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_claude_code_extension",
3
- "version": "1.1.21",
3
+ "version": "1.1.25",
4
4
  "description": "Browse, resume, and manage your Claude Code CLI sessions from a JupyterLab side panel. One click reactivates the right terminal - no duplicate tabs, live remote-control indicator, and favourites for the projects you keep coming back to.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -22,6 +22,7 @@ const session = (over: Partial<ISession> = {}): ISession => ({
22
22
  git_branch: null,
23
23
  remote_control: false,
24
24
  favourite: false,
25
+ extra_sessions: 0,
25
26
  ...over
26
27
  });
27
28
 
@@ -83,6 +84,44 @@ describe('launch spinner dismiss contract', () => {
83
84
  'utf-8'
84
85
  );
85
86
 
87
+ /**
88
+ * Contract for the cleanup-parallel popup: the dialog opens with the
89
+ * Close button hidden while the POST is in flight (footer display
90
+ * 'none'), shows an indeterminate <progress> bar, then on completion
91
+ * fills the bar and reports success - or drops the bar and shows the
92
+ * error - and restores the footer so the user can dismiss.
93
+ */
94
+ describe('cleanup popup contract', () => {
95
+ const cleanup = (widgetSrc.match(
96
+ /private async _cleanupParallel[\s\S]*?\n \}/
97
+ ) ?? [''])[0];
98
+
99
+ it('creates a progress element in the dialog body', () => {
100
+ expect(cleanup).toMatch(/createElement\('progress'\)/);
101
+ });
102
+
103
+ it('hides the footer during the request and restores it in finally', () => {
104
+ expect(cleanup).toMatch(/footer\.style\.display = 'none'/);
105
+ expect(cleanup).toMatch(/finally[\s\S]*?footer\.style\.display = ''/);
106
+ });
107
+
108
+ it('fills the bar and reports the removed count on success', () => {
109
+ expect(cleanup).toMatch(/bar\.value = 1/);
110
+ expect(cleanup).toMatch(/Removed \$\{data\.removed_count\}/);
111
+ });
112
+
113
+ it('shows an error message styled with the error class on failure', () => {
114
+ expect(cleanup).toMatch(/catch[\s\S]*?bar\.remove\(\)/);
115
+ expect(cleanup).toMatch(/jp-ClaudeSessionsPanel-cleanupError/);
116
+ expect(cleanup).toMatch(/Cleanup failed: /);
117
+ });
118
+
119
+ it('refreshes the session list after a successful cleanup', () => {
120
+ const successBlock = (cleanup.match(/try[\s\S]*?catch/) ?? [''])[0];
121
+ expect(successBlock).toMatch(/await this\._fetch\(\)/);
122
+ });
123
+ });
124
+
86
125
  it('_doResumeInTerminal dismisses spinner via dispose(), not resolve()', () => {
87
126
  expect(widgetSrc).toMatch(/spinner\.dispose\(\)/);
88
127
  expect(widgetSrc).not.toMatch(/spinner\.resolve\(\)/);
package/src/types.ts CHANGED
@@ -13,6 +13,7 @@ export interface ISession {
13
13
  git_branch: string | null;
14
14
  remote_control: boolean;
15
15
  favourite: boolean;
16
+ extra_sessions: number;
16
17
  }
17
18
 
18
19
  export interface ISessionsListResponse {
@@ -42,6 +43,14 @@ export interface IRemoveResponse {
42
43
  removed: string;
43
44
  }
44
45
 
46
+ export interface ICleanupRequest {
47
+ encoded_path: string;
48
+ }
49
+
50
+ export interface ICleanupResponse {
51
+ removed_count: number;
52
+ }
53
+
45
54
  export interface ILaunchTerminalRequest {
46
55
  project_path: string;
47
56
  session_id: string;
package/src/widget.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  import {
25
25
  IFavouriteResponse,
26
26
  ILaunchTerminalResponse,
27
+ ICleanupResponse,
27
28
  IRemoveResponse,
28
29
  ISession,
29
30
  ISessionsListResponse
@@ -406,6 +407,69 @@ export class ClaudeCodeSessionsWidget extends Widget {
406
407
  }
407
408
  }
408
409
 
410
+ private async _cleanupParallel(session: ISession): Promise<void> {
411
+ const body = new Widget();
412
+ body.node.className = 'jp-ClaudeSessionsPanel-cleanupBody';
413
+
414
+ const message = document.createElement('div');
415
+ message.className = 'jp-ClaudeSessionsPanel-cleanupMessage';
416
+ const count = session.extra_sessions;
417
+ message.textContent = `Removing ${count} parallel session${
418
+ count === 1 ? '' : 's'
419
+ }...`;
420
+ body.node.appendChild(message);
421
+
422
+ // No `value` attribute -> indeterminate (animated) while the request is
423
+ // in flight; set to max on completion so the bar reads as finished.
424
+ const bar = document.createElement('progress');
425
+ bar.className = 'jp-ClaudeSessionsPanel-cleanupProgress';
426
+ bar.max = 1;
427
+ body.node.appendChild(bar);
428
+
429
+ const dialog = new Dialog<unknown>({
430
+ title: 'Clean Up Parallel Sessions',
431
+ body,
432
+ buttons: [Dialog.okButton({ label: 'Close' })]
433
+ });
434
+ // Hide the Close button while work is in progress; restore it once the
435
+ // outcome (success or error) is shown so the user dismisses the popup.
436
+ const footer = dialog.node.querySelector(
437
+ '.jp-Dialog-footer'
438
+ ) as HTMLElement | null;
439
+ if (footer) {
440
+ footer.style.display = 'none';
441
+ }
442
+ void dialog.launch();
443
+
444
+ try {
445
+ const data = await requestAPI<ICleanupResponse>(
446
+ 'sessions/cleanup',
447
+ this._serverSettings,
448
+ {
449
+ method: 'POST',
450
+ body: JSON.stringify({ encoded_path: session.encoded_path })
451
+ }
452
+ );
453
+ bar.value = 1;
454
+ message.textContent = `Removed ${data.removed_count} parallel session${
455
+ data.removed_count === 1 ? '' : 's'
456
+ }.`;
457
+ // Refresh so the row's extra_sessions count (and menu label) update
458
+ await this._fetch();
459
+ } catch (err) {
460
+ bar.remove();
461
+ message.classList.add('jp-ClaudeSessionsPanel-cleanupError');
462
+ message.textContent = `Cleanup failed: ${
463
+ err instanceof Error ? err.message : String(err)
464
+ }`;
465
+ this._showError(err);
466
+ } finally {
467
+ if (footer) {
468
+ footer.style.display = '';
469
+ }
470
+ }
471
+ }
472
+
409
473
  // -------------------------------------------------------------- terminal
410
474
 
411
475
  private async _resumeInTerminal(
@@ -1039,6 +1103,17 @@ export class ClaudeCodeSessionsWidget extends Widget {
1039
1103
  }
1040
1104
  });
1041
1105
 
1106
+ this._commands.addCommand('claude-code-sessions:cleanup-parallel', {
1107
+ label: () =>
1108
+ `Clean Up Parallel Sessions (${this._activeSession?.extra_sessions ?? 0})`,
1109
+ isVisible: () => (this._activeSession?.extra_sessions ?? 0) > 0,
1110
+ execute: () => {
1111
+ if (this._activeSession) {
1112
+ void this._cleanupParallel(this._activeSession);
1113
+ }
1114
+ }
1115
+ });
1116
+
1042
1117
  this._commands.addCommand('claude-code-sessions:remove', {
1043
1118
  label: 'Remove from Claude',
1044
1119
  icon: removeIcon,
@@ -1066,6 +1141,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1066
1141
  });
1067
1142
  this._contextMenu.addItem({ command: 'claude-code-sessions:copy-path' });
1068
1143
  this._contextMenu.addItem({ type: 'separator' });
1144
+ this._contextMenu.addItem({
1145
+ command: 'claude-code-sessions:cleanup-parallel'
1146
+ });
1069
1147
  this._contextMenu.addItem({ command: 'claude-code-sessions:remove' });
1070
1148
 
1071
1149
  this._contextMenu.aboutToClose.connect(() => {
package/style/base.css CHANGED
@@ -261,6 +261,24 @@
261
261
  border-width: 3px;
262
262
  }
263
263
 
264
+ .jp-ClaudeSessionsPanel-cleanupBody {
265
+ display: flex;
266
+ flex-direction: column;
267
+ align-items: center;
268
+ justify-content: center;
269
+ gap: 12px;
270
+ padding: 12px 8px;
271
+ min-width: 280px;
272
+ }
273
+
274
+ .jp-ClaudeSessionsPanel-cleanupProgress {
275
+ width: 100%;
276
+ }
277
+
278
+ .jp-ClaudeSessionsPanel-cleanupError {
279
+ color: var(--jp-error-color1);
280
+ }
281
+
264
282
  .jp-ClaudeSessionsPanel-row.jp-mod-busy {
265
283
  opacity: 0.55;
266
284
  pointer-events: none;