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 +3 -2
- package/lib/types.d.ts +7 -0
- package/lib/widget.d.ts +1 -0
- package/lib/widget.js +60 -0
- package/package.json +1 -1
- package/src/__tests__/jupyterlab_claude_code_extension.spec.ts +39 -0
- package/src/types.ts +9 -0
- package/src/widget.ts +78 -0
- package/style/base.css +18 -0
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
|
-
- **
|
|
23
|
-
- **
|
|
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
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.
|
|
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;
|