jupyterlab_claude_code_extension 1.2.19 → 1.2.21

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
@@ -29,9 +29,9 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
29
29
  - **Live indicator** - a green dot marks sessions that are currently running somewhere
30
30
  - **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
31
31
  - **Favorites** - star projects you keep coming back to via the right-click menu
32
- - **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
33
- - **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
34
- - **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short session id, e.g. `home (3f2a1b9c)`, with last-activity time; pick one and it becomes the row's current conversation - the next click resumes exactly that one. The submenu shows the 5 most recent; "Manage Sessions..." opens a searchable popup over the full list where conversations can also be deleted - select one, many, or all via checkboxes, then confirm with a two-step Delete button (removed files honour the trash setting). Rows with multiple conversations show a branch icon with the count after the name
32
+ - **Remove** - drop a project's Claude history from the panel via the right-click menu; a confirmation dialog names the project and warns it removes the whole project and all its conversations before anything is touched, then the history folder is moved to the trash (it honours JupyterLab's "move files to trash" setting), not deleted permanently
33
+ - **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; a confirmation dialog naming the project and the count appears first, and removed files honour the same trash setting
34
+ - **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short session id, e.g. `home (3f2a1b9c)`, with last-activity time; pick one and it becomes the row's current conversation - the next click resumes exactly that one. The submenu shows the 5 most recent; "Manage Sessions..." opens a searchable popup over the full list where conversations can also be deleted - select one, many, or all via checkboxes, then confirm in a dialog that names the project and the count (removed files honour the trash setting). Rows with multiple conversations show a branch icon with the count after the name
35
35
  - **Branch session** - fork the current conversation into a new named session via the right-click menu (normal or skip-permissions mode); uses Claude's native `--fork-session`, opens in a new terminal, the chosen name is stamped automatically, and the fork becomes the row's current conversation
36
36
  - **Activity at a glance** - each row shows its last activity (`now`, `5m ago`, `2h ago`, `3d ago`) in an aligned column, with the favourite star in its own column beside it; rows active within the last minute light up in the theme's brand colour (including the `now` label), rows idle for over a week dim slightly
37
37
  - **Search** - fuzzy filter toggled by the funnel button next to refresh
package/lib/widget.js CHANGED
@@ -331,6 +331,16 @@ export class ClaudeCodeSessionsWidget extends Widget {
331
331
  }
332
332
  async _remove(session) {
333
333
  var _a;
334
+ const name = this._lookupName(session);
335
+ const confirm = await showDialog({
336
+ title: 'Remove from Claude',
337
+ body: `Remove "${name}" from Claude? This deletes the entire Claude ` +
338
+ 'project and every conversation it holds. This cannot be undone.',
339
+ buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })]
340
+ });
341
+ if (!confirm.button.accept) {
342
+ return;
343
+ }
334
344
  this._removingPaths.add(session.encoded_path);
335
345
  this._render();
336
346
  try {
@@ -350,6 +360,18 @@ export class ClaudeCodeSessionsWidget extends Widget {
350
360
  }
351
361
  }
352
362
  async _cleanupParallel(session) {
363
+ const extra = session.extra_sessions;
364
+ const name = this._lookupName(session);
365
+ const confirm = await showDialog({
366
+ title: 'Clean Up Parallel Sessions',
367
+ body: `Remove ${extra} parallel session${extra === 1 ? '' : 's'} from ` +
368
+ `"${name}"? The main conversation is kept; the rest are moved to ` +
369
+ 'trash. This cannot be undone.',
370
+ buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })]
371
+ });
372
+ if (!confirm.button.accept) {
373
+ return;
374
+ }
353
375
  const body = new Widget();
354
376
  body.node.className = 'jp-ClaudeSessionsPanel-cleanupBody';
355
377
  const message = document.createElement('div');
@@ -1221,7 +1243,6 @@ export class ClaudeCodeSessionsWidget extends Widget {
1221
1243
  // Local working copy so deletions can refresh the list in place.
1222
1244
  let items = [...branches];
1223
1245
  const selected = new Set();
1224
- let confirmArmed = false;
1225
1246
  const body = document.createElement('div');
1226
1247
  body.className = 'jp-ClaudeSessionsPanel-branchPopup';
1227
1248
  const search = document.createElement('input');
@@ -1257,12 +1278,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1257
1278
  b.label.toLowerCase().includes(needle) ||
1258
1279
  b.session_id.toLowerCase().includes(needle));
1259
1280
  };
1260
- // Any selection change disarms a pending confirm.
1261
1281
  const updateControls = () => {
1262
- confirmArmed = false;
1263
1282
  deleteBtn.disabled = selected.size === 0;
1264
1283
  deleteBtn.textContent = `Delete (${selected.size})`;
1265
- deleteBtn.classList.remove('jp-mod-confirm');
1266
1284
  const visible = visibleMatches();
1267
1285
  const visibleSelected = visible.filter(b => selected.has(b.session_id)).length;
1268
1286
  selectAll.checked =
@@ -1362,22 +1380,26 @@ export class ClaudeCodeSessionsWidget extends Widget {
1362
1380
  if (selected.size === 0) {
1363
1381
  return;
1364
1382
  }
1365
- if (!confirmArmed) {
1366
- // Two-step delete: first click arms, second click executes.
1367
- confirmArmed = true;
1368
- deleteBtn.textContent = `Confirm delete (${selected.size})`;
1369
- deleteBtn.classList.add('jp-mod-confirm');
1370
- return;
1371
- }
1372
- void this._deleteBranches([...selected]).then(deleted => {
1373
- if (deleted === null) {
1383
+ const n = selected.size;
1384
+ void showDialog({
1385
+ title: 'Delete Sessions',
1386
+ body: `Delete ${n} session${n === 1 ? '' : 's'} from "${this._activeSession ? this._lookupName(this._activeSession) : ''}"? ` +
1387
+ 'They are moved to trash. This cannot be undone.',
1388
+ buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Delete' })]
1389
+ }).then(confirm => {
1390
+ if (!confirm.button.accept) {
1374
1391
  return;
1375
1392
  }
1376
- items = items.filter(b => !selected.has(b.session_id));
1377
- selected.clear();
1378
- this._lastBranches = items;
1379
- render();
1380
- updateControls();
1393
+ void this._deleteBranches([...selected]).then(deleted => {
1394
+ if (deleted === null) {
1395
+ return;
1396
+ }
1397
+ items = items.filter(b => !selected.has(b.session_id));
1398
+ selected.clear();
1399
+ this._lastBranches = items;
1400
+ render();
1401
+ updateControls();
1402
+ });
1381
1403
  });
1382
1404
  });
1383
1405
  search.addEventListener('input', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_claude_code_extension",
3
- "version": "1.2.19",
3
+ "version": "1.2.21",
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",
@@ -96,6 +96,14 @@ describe('launch spinner dismiss contract', () => {
96
96
  /private async _cleanupParallel[\s\S]*?\n \}/
97
97
  ) ?? [''])[0];
98
98
 
99
+ it('confirms before removing and aborts when not accepted', () => {
100
+ expect(cleanup).toMatch(
101
+ /showDialog\(\{\s*title: 'Clean Up Parallel Sessions'/
102
+ );
103
+ expect(cleanup).toMatch(/Dialog\.warnButton\(\{ label: 'Remove' \}\)/);
104
+ expect(cleanup).toMatch(/if \(!confirm\.button\.accept\) \{\s*return;/);
105
+ });
106
+
99
107
  it('creates a progress element in the dialog body', () => {
100
108
  expect(cleanup).toMatch(/createElement\('progress'\)/);
101
109
  });
@@ -232,15 +240,14 @@ describe('launch spinner dismiss contract', () => {
232
240
  );
233
241
  });
234
242
 
235
- it('delete is two-step and any selection change disarms it', () => {
243
+ it('delete opens a confirmation dialog and only deletes on accept', () => {
236
244
  const popup = (widgetSrc.match(
237
245
  /private _showBranchPopup[\s\S]*?\n \}/
238
246
  ) ?? [''])[0];
239
- expect(popup).toMatch(/`Confirm delete \(\$\{selected\.size\}\)`/);
240
- // updateControls (run on every selection change) resets the armed
241
- // state and the button label.
247
+ expect(popup).toMatch(/showDialog\(\{\s*title: 'Delete Sessions'/);
248
+ expect(popup).toMatch(/Dialog\.warnButton\(\{ label: 'Delete' \}\)/);
242
249
  expect(popup).toMatch(
243
- /const updateControls = \(\) => \{\s*confirmArmed = false/
250
+ /if \(!confirm\.button\.accept\) \{\s*return;[\s\S]*?_deleteBranches/
244
251
  );
245
252
  });
246
253
 
package/src/widget.ts CHANGED
@@ -407,6 +407,18 @@ export class ClaudeCodeSessionsWidget extends Widget {
407
407
  }
408
408
 
409
409
  private async _remove(session: ISession): Promise<void> {
410
+ const name = this._lookupName(session);
411
+ const confirm = await showDialog({
412
+ title: 'Remove from Claude',
413
+ body:
414
+ `Remove "${name}" from Claude? This deletes the entire Claude ` +
415
+ 'project and every conversation it holds. This cannot be undone.',
416
+ buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })]
417
+ });
418
+ if (!confirm.button.accept) {
419
+ return;
420
+ }
421
+
410
422
  this._removingPaths.add(session.encoded_path);
411
423
  this._render();
412
424
  try {
@@ -431,6 +443,20 @@ export class ClaudeCodeSessionsWidget extends Widget {
431
443
  }
432
444
 
433
445
  private async _cleanupParallel(session: ISession): Promise<void> {
446
+ const extra = session.extra_sessions;
447
+ const name = this._lookupName(session);
448
+ const confirm = await showDialog({
449
+ title: 'Clean Up Parallel Sessions',
450
+ body:
451
+ `Remove ${extra} parallel session${extra === 1 ? '' : 's'} from ` +
452
+ `"${name}"? The main conversation is kept; the rest are moved to ` +
453
+ 'trash. This cannot be undone.',
454
+ buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })]
455
+ });
456
+ if (!confirm.button.accept) {
457
+ return;
458
+ }
459
+
434
460
  const body = new Widget();
435
461
  body.node.className = 'jp-ClaudeSessionsPanel-cleanupBody';
436
462
 
@@ -1420,7 +1446,6 @@ export class ClaudeCodeSessionsWidget extends Widget {
1420
1446
  // Local working copy so deletions can refresh the list in place.
1421
1447
  let items = [...branches];
1422
1448
  const selected = new Set<string>();
1423
- let confirmArmed = false;
1424
1449
 
1425
1450
  const body = document.createElement('div');
1426
1451
  body.className = 'jp-ClaudeSessionsPanel-branchPopup';
@@ -1467,12 +1492,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1467
1492
  );
1468
1493
  };
1469
1494
 
1470
- // Any selection change disarms a pending confirm.
1471
1495
  const updateControls = () => {
1472
- confirmArmed = false;
1473
1496
  deleteBtn.disabled = selected.size === 0;
1474
1497
  deleteBtn.textContent = `Delete (${selected.size})`;
1475
- deleteBtn.classList.remove('jp-mod-confirm');
1476
1498
  const visible = visibleMatches();
1477
1499
  const visibleSelected = visible.filter(b =>
1478
1500
  selected.has(b.session_id)
@@ -1580,22 +1602,27 @@ export class ClaudeCodeSessionsWidget extends Widget {
1580
1602
  if (selected.size === 0) {
1581
1603
  return;
1582
1604
  }
1583
- if (!confirmArmed) {
1584
- // Two-step delete: first click arms, second click executes.
1585
- confirmArmed = true;
1586
- deleteBtn.textContent = `Confirm delete (${selected.size})`;
1587
- deleteBtn.classList.add('jp-mod-confirm');
1588
- return;
1589
- }
1590
- void this._deleteBranches([...selected]).then(deleted => {
1591
- if (deleted === null) {
1605
+ const n = selected.size;
1606
+ void showDialog({
1607
+ title: 'Delete Sessions',
1608
+ body:
1609
+ `Delete ${n} session${n === 1 ? '' : 's'} from "${this._activeSession ? this._lookupName(this._activeSession) : ''}"? ` +
1610
+ 'They are moved to trash. This cannot be undone.',
1611
+ buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Delete' })]
1612
+ }).then(confirm => {
1613
+ if (!confirm.button.accept) {
1592
1614
  return;
1593
1615
  }
1594
- items = items.filter(b => !selected.has(b.session_id));
1595
- selected.clear();
1596
- this._lastBranches = items;
1597
- render();
1598
- updateControls();
1616
+ void this._deleteBranches([...selected]).then(deleted => {
1617
+ if (deleted === null) {
1618
+ return;
1619
+ }
1620
+ items = items.filter(b => !selected.has(b.session_id));
1621
+ selected.clear();
1622
+ this._lastBranches = items;
1623
+ render();
1624
+ updateControls();
1625
+ });
1599
1626
  });
1600
1627
  });
1601
1628