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 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 = 10000;
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
- this._fetch()
89
- .catch(err => this._showError(err))
90
- .finally(() => this._setRefreshSpinning(false));
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
- const data = await requestAPI('sessions', this._serverSettings);
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._app.shell.activateById(cached.id);
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._app.shell.activateById(found.id);
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.56",
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 = 10_000;
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
- this._fetch()
117
- .catch(err => this._showError(err))
118
- .finally(() => this._setRefreshSpinning(false));
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._app.shell.activateById(cached.id);
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._app.shell.activateById(found.id);
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> {