jupyterlab_claude_code_extension 1.0.56 → 1.0.58
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 +1 -0
- package/lib/widget.d.ts +9 -0
- package/lib/widget.js +46 -11
- package/package.json +1 -1
- package/src/widget.ts +47 -11
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Browse, resume, and manage your Claude Code sessions from a JupyterLab side pane
|
|
|
18
18
|
- **Live indicator** - a green dot marks sessions that are currently running somewhere
|
|
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
|
+
- **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
|
|
21
22
|
- **Search** - fuzzy filter at the top of the panel
|
|
22
23
|
- **Presentation modes** - label rows by session name, folder name, or path relative to the JupyterLab root
|
|
23
24
|
- **Hover tooltip** with project path, last activity, message count, branch, and session id
|
package/lib/widget.d.ts
CHANGED
|
@@ -38,6 +38,15 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
38
38
|
private _remove;
|
|
39
39
|
private _resumeInTerminal;
|
|
40
40
|
private _doResumeInTerminal;
|
|
41
|
+
/**
|
|
42
|
+
* Bring a terminal tab to the front AND hand it keyboard focus, so the
|
|
43
|
+
* user can start typing without an extra click. `activateById` only
|
|
44
|
+
* reveals the tab; the xterm inside doesn't always grab DOM focus,
|
|
45
|
+
* especially when the click originated in this sidebar. We defer the
|
|
46
|
+
* `term.focus()` to the next frame so the widget is attached and visible
|
|
47
|
+
* first.
|
|
48
|
+
*/
|
|
49
|
+
private _focusTerminal;
|
|
41
50
|
private _findTerminalForCwd;
|
|
42
51
|
private _showCloseExistingDialog;
|
|
43
52
|
/** Show a modal with a spinner while the terminal is being launched. The
|
package/lib/widget.js
CHANGED
|
@@ -3,7 +3,7 @@ import { CommandRegistry } from '@lumino/commands';
|
|
|
3
3
|
import { Menu, Widget } from '@lumino/widgets';
|
|
4
4
|
import { requestAPI } from './request';
|
|
5
5
|
import { claudeIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon } from './icons';
|
|
6
|
-
const POLL_INTERVAL_MS =
|
|
6
|
+
const POLL_INTERVAL_MS = 30000;
|
|
7
7
|
const DEFAULT_RECENT_LIMIT = 10;
|
|
8
8
|
const EXPANDED_STORAGE_KEY = 'jupyterlab_claude_code_extension:expanded';
|
|
9
9
|
const DEFAULT_PRESENTATION_MODE = 'session';
|
|
@@ -85,9 +85,14 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
85
85
|
refresh() {
|
|
86
86
|
this._showLoading();
|
|
87
87
|
this._setRefreshSpinning(true);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
// `_fetch` is filesystem-fast, so without a floor the refresh icon would
|
|
89
|
+
// spin for a single frame and read as "nothing happened". Hold the
|
|
90
|
+
// spinner for at least ~500 ms so the click visibly registers.
|
|
91
|
+
const minSpin = new Promise(resolve => window.setTimeout(resolve, 500));
|
|
92
|
+
Promise.all([
|
|
93
|
+
this._fetch().catch(err => this._showError(err)),
|
|
94
|
+
minSpin
|
|
95
|
+
]).finally(() => this._setRefreshSpinning(false));
|
|
91
96
|
}
|
|
92
97
|
/** Choose how rows are labelled: by session name, folder name, or path. */
|
|
93
98
|
setPresentationMode(mode) {
|
|
@@ -244,7 +249,10 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
244
249
|
// ------------------------------------------------------------------ data
|
|
245
250
|
async _fetch() {
|
|
246
251
|
var _a;
|
|
247
|
-
|
|
252
|
+
// `cache: 'no-store'` so the manual refresh button (and the post-launch
|
|
253
|
+
// refresh) always re-read the server's view of ~/.claude rather than a
|
|
254
|
+
// possibly-stale browser-cached response.
|
|
255
|
+
const data = await requestAPI('sessions', this._serverSettings, { cache: 'no-store' });
|
|
248
256
|
this._sessions = (_a = data.sessions) !== null && _a !== void 0 ? _a : [];
|
|
249
257
|
this._render();
|
|
250
258
|
}
|
|
@@ -315,10 +323,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
315
323
|
if (cached && !cached.isDisposed) {
|
|
316
324
|
if (forceDangerous) {
|
|
317
325
|
await this._showCloseExistingDialog();
|
|
318
|
-
this._app.shell.activateById(cached.id);
|
|
319
|
-
return;
|
|
320
326
|
}
|
|
321
|
-
this.
|
|
327
|
+
this._focusTerminal(cached);
|
|
322
328
|
return;
|
|
323
329
|
}
|
|
324
330
|
// 2. Walk every live terminal widget JL knows about.
|
|
@@ -328,10 +334,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
328
334
|
this._wireTerminalDisposal(session.project_path, found);
|
|
329
335
|
if (forceDangerous) {
|
|
330
336
|
await this._showCloseExistingDialog();
|
|
331
|
-
this._app.shell.activateById(found.id);
|
|
332
|
-
return;
|
|
333
337
|
}
|
|
334
|
-
this.
|
|
338
|
+
this._focusTerminal(found);
|
|
335
339
|
return;
|
|
336
340
|
}
|
|
337
341
|
// 3. No matching terminal - spawn a new one with `claude --resume <id>`
|
|
@@ -357,6 +361,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
357
361
|
if (widget === null || widget === void 0 ? void 0 : widget.id) {
|
|
358
362
|
this._terminalsByPath.set(session.project_path, widget);
|
|
359
363
|
this._wireTerminalDisposal(session.project_path, widget);
|
|
364
|
+
this._focusTerminal(widget);
|
|
360
365
|
}
|
|
361
366
|
}
|
|
362
367
|
finally {
|
|
@@ -366,6 +371,36 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
366
371
|
catch (err) {
|
|
367
372
|
this._showError(err);
|
|
368
373
|
}
|
|
374
|
+
finally {
|
|
375
|
+
// Reuse or fresh launch, either way the picture changed (a session may
|
|
376
|
+
// now be remote-controlled, a row may have appeared). Pull fresh state.
|
|
377
|
+
void this._fetch().catch(() => {
|
|
378
|
+
/* a poll tick will retry; nothing actionable here */
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Bring a terminal tab to the front AND hand it keyboard focus, so the
|
|
384
|
+
* user can start typing without an extra click. `activateById` only
|
|
385
|
+
* reveals the tab; the xterm inside doesn't always grab DOM focus,
|
|
386
|
+
* especially when the click originated in this sidebar. We defer the
|
|
387
|
+
* `term.focus()` to the next frame so the widget is attached and visible
|
|
388
|
+
* first.
|
|
389
|
+
*/
|
|
390
|
+
_focusTerminal(widget) {
|
|
391
|
+
if (!widget || widget.isDisposed) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
this._app.shell.activateById(widget.id);
|
|
395
|
+
requestAnimationFrame(() => {
|
|
396
|
+
var _a, _b, _c;
|
|
397
|
+
try {
|
|
398
|
+
(_c = (_b = (_a = widget.content) === null || _a === void 0 ? void 0 : _a.term) === null || _b === void 0 ? void 0 : _b.focus) === null || _c === void 0 ? void 0 : _c.call(_b);
|
|
399
|
+
}
|
|
400
|
+
catch (_err) {
|
|
401
|
+
/* terminal may have been disposed in the meantime - ignore */
|
|
402
|
+
}
|
|
403
|
+
});
|
|
369
404
|
}
|
|
370
405
|
async _findTerminalForCwd(projectPath) {
|
|
371
406
|
var _a, _b;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_claude_code_extension",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.58",
|
|
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",
|
package/src/widget.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
ISessionsListResponse
|
|
23
23
|
} from './types';
|
|
24
24
|
|
|
25
|
-
const POLL_INTERVAL_MS =
|
|
25
|
+
const POLL_INTERVAL_MS = 30_000;
|
|
26
26
|
const DEFAULT_RECENT_LIMIT = 10;
|
|
27
27
|
const EXPANDED_STORAGE_KEY = 'jupyterlab_claude_code_extension:expanded';
|
|
28
28
|
|
|
@@ -113,9 +113,16 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
113
113
|
refresh(): void {
|
|
114
114
|
this._showLoading();
|
|
115
115
|
this._setRefreshSpinning(true);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
// `_fetch` is filesystem-fast, so without a floor the refresh icon would
|
|
117
|
+
// spin for a single frame and read as "nothing happened". Hold the
|
|
118
|
+
// spinner for at least ~500 ms so the click visibly registers.
|
|
119
|
+
const minSpin = new Promise<void>(resolve =>
|
|
120
|
+
window.setTimeout(resolve, 500)
|
|
121
|
+
);
|
|
122
|
+
Promise.all([
|
|
123
|
+
this._fetch().catch(err => this._showError(err)),
|
|
124
|
+
minSpin
|
|
125
|
+
]).finally(() => this._setRefreshSpinning(false));
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
/** Choose how rows are labelled: by session name, folder name, or path. */
|
|
@@ -296,9 +303,13 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
296
303
|
// ------------------------------------------------------------------ data
|
|
297
304
|
|
|
298
305
|
private async _fetch(): Promise<void> {
|
|
306
|
+
// `cache: 'no-store'` so the manual refresh button (and the post-launch
|
|
307
|
+
// refresh) always re-read the server's view of ~/.claude rather than a
|
|
308
|
+
// possibly-stale browser-cached response.
|
|
299
309
|
const data = await requestAPI<ISessionsListResponse>(
|
|
300
310
|
'sessions',
|
|
301
|
-
this._serverSettings
|
|
311
|
+
this._serverSettings,
|
|
312
|
+
{ cache: 'no-store' }
|
|
302
313
|
);
|
|
303
314
|
this._sessions = data.sessions ?? [];
|
|
304
315
|
this._render();
|
|
@@ -390,10 +401,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
390
401
|
if (cached && !cached.isDisposed) {
|
|
391
402
|
if (forceDangerous) {
|
|
392
403
|
await this._showCloseExistingDialog();
|
|
393
|
-
this._app.shell.activateById(cached.id);
|
|
394
|
-
return;
|
|
395
404
|
}
|
|
396
|
-
this.
|
|
405
|
+
this._focusTerminal(cached);
|
|
397
406
|
return;
|
|
398
407
|
}
|
|
399
408
|
|
|
@@ -404,10 +413,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
404
413
|
this._wireTerminalDisposal(session.project_path, found);
|
|
405
414
|
if (forceDangerous) {
|
|
406
415
|
await this._showCloseExistingDialog();
|
|
407
|
-
this._app.shell.activateById(found.id);
|
|
408
|
-
return;
|
|
409
416
|
}
|
|
410
|
-
this.
|
|
417
|
+
this._focusTerminal(found);
|
|
411
418
|
return;
|
|
412
419
|
}
|
|
413
420
|
|
|
@@ -441,13 +448,42 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
441
448
|
if (widget?.id) {
|
|
442
449
|
this._terminalsByPath.set(session.project_path, widget);
|
|
443
450
|
this._wireTerminalDisposal(session.project_path, widget);
|
|
451
|
+
this._focusTerminal(widget);
|
|
444
452
|
}
|
|
445
453
|
} finally {
|
|
446
454
|
spinner.resolve();
|
|
447
455
|
}
|
|
448
456
|
} catch (err) {
|
|
449
457
|
this._showError(err);
|
|
458
|
+
} finally {
|
|
459
|
+
// Reuse or fresh launch, either way the picture changed (a session may
|
|
460
|
+
// now be remote-controlled, a row may have appeared). Pull fresh state.
|
|
461
|
+
void this._fetch().catch(() => {
|
|
462
|
+
/* a poll tick will retry; nothing actionable here */
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Bring a terminal tab to the front AND hand it keyboard focus, so the
|
|
469
|
+
* user can start typing without an extra click. `activateById` only
|
|
470
|
+
* reveals the tab; the xterm inside doesn't always grab DOM focus,
|
|
471
|
+
* especially when the click originated in this sidebar. We defer the
|
|
472
|
+
* `term.focus()` to the next frame so the widget is attached and visible
|
|
473
|
+
* first.
|
|
474
|
+
*/
|
|
475
|
+
private _focusTerminal(widget: any): void {
|
|
476
|
+
if (!widget || widget.isDisposed) {
|
|
477
|
+
return;
|
|
450
478
|
}
|
|
479
|
+
this._app.shell.activateById(widget.id);
|
|
480
|
+
requestAnimationFrame(() => {
|
|
481
|
+
try {
|
|
482
|
+
widget.content?.term?.focus?.();
|
|
483
|
+
} catch (_err) {
|
|
484
|
+
/* terminal may have been disposed in the meantime - ignore */
|
|
485
|
+
}
|
|
486
|
+
});
|
|
451
487
|
}
|
|
452
488
|
|
|
453
489
|
private async _findTerminalForCwd(projectPath: string): Promise<any | null> {
|