jupyterlab_claude_code_extension 1.2.20 → 1.2.22

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
@@ -30,8 +30,8 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
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
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; 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
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.d.ts CHANGED
@@ -121,12 +121,14 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
121
121
  /** Fork the active row's current conversation into a new named branch.
122
122
  *
123
123
  * Asks for a name, then launches a terminal running
124
- * ``claude --resume <current> --fork-session --session-id <new uuid>`` -
125
- * the uuid is generated here so the forked JSONL is known up front. Once
126
- * claude materialises the file (polled via sessions/set-title) the chosen
127
- * name is stamped as a custom-title record. The fork is the newest JSONL,
128
- * so the recency resolution makes it the row's current conversation
129
- * without an explicit switch.
124
+ * ``claude --resume <current> --fork-session --session-id <new uuid> -n <name>`` -
125
+ * the uuid is generated here so the forked JSONL is known up front, and
126
+ * ``-n`` makes claude own the name: it writes the chosen name as a
127
+ * custom-title record on its first turn and re-stamps it every turn, so
128
+ * it sticks even though the fork inherits the parent's title. The fork is
129
+ * the newest JSONL, so recency resolution makes it the row's current
130
+ * conversation without an explicit switch. ``_stampForkTitle`` still runs
131
+ * to seed the title and refresh the row promptly while claude starts.
130
132
  */
131
133
  private _branchSession;
132
134
  /** Retry sessions/set-title until the forked JSONL exists (404 while it
package/lib/widget.js CHANGED
@@ -360,6 +360,18 @@ export class ClaudeCodeSessionsWidget extends Widget {
360
360
  }
361
361
  }
362
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
+ }
363
375
  const body = new Widget();
364
376
  body.node.className = 'jp-ClaudeSessionsPanel-cleanupBody';
365
377
  const message = document.createElement('div');
@@ -1231,7 +1243,6 @@ export class ClaudeCodeSessionsWidget extends Widget {
1231
1243
  // Local working copy so deletions can refresh the list in place.
1232
1244
  let items = [...branches];
1233
1245
  const selected = new Set();
1234
- let confirmArmed = false;
1235
1246
  const body = document.createElement('div');
1236
1247
  body.className = 'jp-ClaudeSessionsPanel-branchPopup';
1237
1248
  const search = document.createElement('input');
@@ -1267,12 +1278,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1267
1278
  b.label.toLowerCase().includes(needle) ||
1268
1279
  b.session_id.toLowerCase().includes(needle));
1269
1280
  };
1270
- // Any selection change disarms a pending confirm.
1271
1281
  const updateControls = () => {
1272
- confirmArmed = false;
1273
1282
  deleteBtn.disabled = selected.size === 0;
1274
1283
  deleteBtn.textContent = `Delete (${selected.size})`;
1275
- deleteBtn.classList.remove('jp-mod-confirm');
1276
1284
  const visible = visibleMatches();
1277
1285
  const visibleSelected = visible.filter(b => selected.has(b.session_id)).length;
1278
1286
  selectAll.checked =
@@ -1372,22 +1380,26 @@ export class ClaudeCodeSessionsWidget extends Widget {
1372
1380
  if (selected.size === 0) {
1373
1381
  return;
1374
1382
  }
1375
- if (!confirmArmed) {
1376
- // Two-step delete: first click arms, second click executes.
1377
- confirmArmed = true;
1378
- deleteBtn.textContent = `Confirm delete (${selected.size})`;
1379
- deleteBtn.classList.add('jp-mod-confirm');
1380
- return;
1381
- }
1382
- void this._deleteBranches([...selected]).then(deleted => {
1383
- 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) {
1384
1391
  return;
1385
1392
  }
1386
- items = items.filter(b => !selected.has(b.session_id));
1387
- selected.clear();
1388
- this._lastBranches = items;
1389
- render();
1390
- 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
+ });
1391
1403
  });
1392
1404
  });
1393
1405
  search.addEventListener('input', () => {
@@ -1430,12 +1442,14 @@ export class ClaudeCodeSessionsWidget extends Widget {
1430
1442
  /** Fork the active row's current conversation into a new named branch.
1431
1443
  *
1432
1444
  * Asks for a name, then launches a terminal running
1433
- * ``claude --resume <current> --fork-session --session-id <new uuid>`` -
1434
- * the uuid is generated here so the forked JSONL is known up front. Once
1435
- * claude materialises the file (polled via sessions/set-title) the chosen
1436
- * name is stamped as a custom-title record. The fork is the newest JSONL,
1437
- * so the recency resolution makes it the row's current conversation
1438
- * without an explicit switch.
1445
+ * ``claude --resume <current> --fork-session --session-id <new uuid> -n <name>`` -
1446
+ * the uuid is generated here so the forked JSONL is known up front, and
1447
+ * ``-n`` makes claude own the name: it writes the chosen name as a
1448
+ * custom-title record on its first turn and re-stamps it every turn, so
1449
+ * it sticks even though the fork inherits the parent's title. The fork is
1450
+ * the newest JSONL, so recency resolution makes it the row's current
1451
+ * conversation without an explicit switch. ``_stampForkTitle`` still runs
1452
+ * to seed the title and refresh the row promptly while claude starts.
1439
1453
  */
1440
1454
  async _branchSession(forceDangerous) {
1441
1455
  const session = this._activeSession;
@@ -1460,6 +1474,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
1460
1474
  project_path: session.project_path,
1461
1475
  session_id: session.session_id,
1462
1476
  fork_session_id: forkId,
1477
+ name: title,
1463
1478
  dangerously_skip_permissions: forceDangerous || this._dangerouslySkip
1464
1479
  })
1465
1480
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_claude_code_extension",
3
- "version": "1.2.20",
3
+ "version": "1.2.22",
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
 
@@ -371,6 +378,8 @@ describe('launch spinner dismiss contract', () => {
371
378
  expect(branch).toMatch(/UUID\.uuid4\(\)/);
372
379
  expect(branch).toMatch(/fork_session_id: forkId/);
373
380
  expect(branch).toMatch(/session_id: session\.session_id/);
381
+ // The name is forced at launch (claude -n <name>) so it sticks.
382
+ expect(branch).toMatch(/name: title/);
374
383
  expect(branch).toMatch(/_stampForkTitle/);
375
384
  });
376
385
 
package/src/widget.ts CHANGED
@@ -443,6 +443,20 @@ export class ClaudeCodeSessionsWidget extends Widget {
443
443
  }
444
444
 
445
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
+
446
460
  const body = new Widget();
447
461
  body.node.className = 'jp-ClaudeSessionsPanel-cleanupBody';
448
462
 
@@ -1432,7 +1446,6 @@ export class ClaudeCodeSessionsWidget extends Widget {
1432
1446
  // Local working copy so deletions can refresh the list in place.
1433
1447
  let items = [...branches];
1434
1448
  const selected = new Set<string>();
1435
- let confirmArmed = false;
1436
1449
 
1437
1450
  const body = document.createElement('div');
1438
1451
  body.className = 'jp-ClaudeSessionsPanel-branchPopup';
@@ -1479,12 +1492,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1479
1492
  );
1480
1493
  };
1481
1494
 
1482
- // Any selection change disarms a pending confirm.
1483
1495
  const updateControls = () => {
1484
- confirmArmed = false;
1485
1496
  deleteBtn.disabled = selected.size === 0;
1486
1497
  deleteBtn.textContent = `Delete (${selected.size})`;
1487
- deleteBtn.classList.remove('jp-mod-confirm');
1488
1498
  const visible = visibleMatches();
1489
1499
  const visibleSelected = visible.filter(b =>
1490
1500
  selected.has(b.session_id)
@@ -1592,22 +1602,27 @@ export class ClaudeCodeSessionsWidget extends Widget {
1592
1602
  if (selected.size === 0) {
1593
1603
  return;
1594
1604
  }
1595
- if (!confirmArmed) {
1596
- // Two-step delete: first click arms, second click executes.
1597
- confirmArmed = true;
1598
- deleteBtn.textContent = `Confirm delete (${selected.size})`;
1599
- deleteBtn.classList.add('jp-mod-confirm');
1600
- return;
1601
- }
1602
- void this._deleteBranches([...selected]).then(deleted => {
1603
- 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) {
1604
1614
  return;
1605
1615
  }
1606
- items = items.filter(b => !selected.has(b.session_id));
1607
- selected.clear();
1608
- this._lastBranches = items;
1609
- render();
1610
- 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
+ });
1611
1626
  });
1612
1627
  });
1613
1628
 
@@ -1656,12 +1671,14 @@ export class ClaudeCodeSessionsWidget extends Widget {
1656
1671
  /** Fork the active row's current conversation into a new named branch.
1657
1672
  *
1658
1673
  * Asks for a name, then launches a terminal running
1659
- * ``claude --resume <current> --fork-session --session-id <new uuid>`` -
1660
- * the uuid is generated here so the forked JSONL is known up front. Once
1661
- * claude materialises the file (polled via sessions/set-title) the chosen
1662
- * name is stamped as a custom-title record. The fork is the newest JSONL,
1663
- * so the recency resolution makes it the row's current conversation
1664
- * without an explicit switch.
1674
+ * ``claude --resume <current> --fork-session --session-id <new uuid> -n <name>`` -
1675
+ * the uuid is generated here so the forked JSONL is known up front, and
1676
+ * ``-n`` makes claude own the name: it writes the chosen name as a
1677
+ * custom-title record on its first turn and re-stamps it every turn, so
1678
+ * it sticks even though the fork inherits the parent's title. The fork is
1679
+ * the newest JSONL, so recency resolution makes it the row's current
1680
+ * conversation without an explicit switch. ``_stampForkTitle`` still runs
1681
+ * to seed the title and refresh the row promptly while claude starts.
1665
1682
  */
1666
1683
  private async _branchSession(forceDangerous: boolean): Promise<void> {
1667
1684
  const session = this._activeSession;
@@ -1689,6 +1706,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
1689
1706
  project_path: session.project_path,
1690
1707
  session_id: session.session_id,
1691
1708
  fork_session_id: forkId,
1709
+ name: title,
1692
1710
  dangerously_skip_permissions:
1693
1711
  forceDangerous || this._dangerouslySkip
1694
1712
  })