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 +2 -2
- package/lib/widget.d.ts +8 -6
- package/lib/widget.js +39 -24
- package/package.json +1 -1
- package/src/__tests__/jupyterlab_claude_code_extension.spec.ts +14 -5
- package/src/widget.ts +42 -24
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
|
|
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
|
|
126
|
-
* claude
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
}
|
|
1382
|
-
|
|
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
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
|
1435
|
-
* claude
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
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.
|
|
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
|
|
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(
|
|
240
|
-
|
|
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
|
-
/
|
|
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
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
if (
|
|
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
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
|
1661
|
-
* claude
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
1664
|
-
*
|
|
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
|
})
|