jupyterlab_claude_code_extension 1.0.50 → 1.0.54

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
@@ -8,21 +8,20 @@
8
8
  [![Brought To You By KOLOMOLO](https://img.shields.io/badge/Brought%20To%20You%20By-KOLOMOLO-00ffff?style=flat)](https://kolomolo.com)
9
9
  [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-blue?style=flat)](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)
10
10
 
11
- Manage Claude Code CLI sessions from inside JupyterLab. A left-sidebar panel lists every project under `~/.claude/projects/` deduplicated to one row per folder, marks live remote-control sessions with a green dot, and lets you jump back into any session by opening (or reactivating) a terminal pwd'd to that project and auto-running `claude --resume <id>`.
11
+ Browse, resume, and manage your Claude Code sessions from a JupyterLab side panel. One click reactivates the right terminal, no duplicate tabs, with a live indicator showing which sessions are currently active.
12
12
 
13
13
  ![Claude Code Sessions panel](.resources/screenshot.png)
14
14
 
15
15
  ## Features
16
16
 
17
- - **Three-section side panel** - Favourites, Recent (top 10 by activity), and All. Each section scrolls independently; Favourites disappears when empty
18
- - **Live remote-control indicator** - green dot on rows whose `~/.claude/sessions/<pid>.json` is alive (verified via `os.kill(pid, 0)`)
19
- - **One-click resume** - click a row to find an existing terminal pwd'd to that project (queried server-side from the pty's process tree) and reactivate its tab; only spawns a fresh terminal if none matches. Concurrent rapid clicks are coalesced
20
- - **Smart name resolution** - shows the user-set `/rename` name when available; auto-detected names (volatile across the same `sessionId` or 3+ token lowercase-kebab) fall back to the folder basename. Toggle the behaviour via the `resolveSessionNames` setting
21
- - **Path-segment disambiguation** - when two sessions share the same display name, the row reveals the minimum number of trailing path segments needed for each to be unique
22
- - **Favourites** - star a session via the right-click menu; persisted server-side at `~/.claude/jupyterlab_claude_code_extension.json`
23
- - **Layout restorer** - panel visibility persists across JupyterLab reloads
24
- - **Auto-disabled** when the `claude` binary is not on `PATH`
25
- - **Hover tooltip** with relative path (vs JL root), last activity, message count, branch, first prompt, session id
17
+ - **Three-section side panel** - Favorites, Recent, and All projects, each scrolling independently
18
+ - **Live indicator** - a green dot marks sessions that are currently running somewhere
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
+ - **Favorites** - star projects you keep coming back to via the right-click menu
21
+ - **Search** - fuzzy filter at the top of the panel
22
+ - **Presentation modes** - label rows by session name, folder name, or path relative to the JupyterLab root
23
+ - **Hover tooltip** with project path, last activity, message count, branch, and session id
24
+ - **Auto-disabled** when the Claude Code CLI is not installed
26
25
 
27
26
  ## Requirements
28
27
 
package/lib/index.js CHANGED
@@ -7,7 +7,7 @@ const PLUGIN_ID = 'jupyterlab_claude_code_extension:plugin';
7
7
  const WIDGET_ID = 'jupyterlab-claude-code-extension';
8
8
  const plugin = {
9
9
  id: PLUGIN_ID,
10
- description: 'Side panel listing Claude Code sessions per project folder, with remote-control indicator, favourites, and one-click resume in a terminal.',
10
+ description: 'Side panel listing Claude Code sessions per project folder, with remote-control indicator, favorites, and one-click resume in a terminal.',
11
11
  autoStart: true,
12
12
  requires: [ILabShell],
13
13
  optional: [ILayoutRestorer, ISettingRegistry, ITerminalTracker],
@@ -26,10 +26,17 @@ const plugin = {
26
26
  return;
27
27
  }
28
28
  const widget = new ClaudeCodeSessionsWidget(app, status.root_dir || '', terminalTracker);
29
- labShell.add(widget, 'left', { rank: 600 });
29
+ // Read the sidebar setting before docking so we add the widget to the
30
+ // user's preferred side on first paint. Default to left.
31
+ let currentSidebar = 'left';
30
32
  if (settingRegistry) {
31
33
  try {
32
34
  const settings = await settingRegistry.load(PLUGIN_ID);
35
+ const initialSidebar = settings.get('sidebar').composite;
36
+ if (initialSidebar === 'left' || initialSidebar === 'right') {
37
+ currentSidebar = initialSidebar;
38
+ }
39
+ labShell.add(widget, currentSidebar, { rank: 600 });
33
40
  const apply = () => {
34
41
  const mode = settings.get('presentationMode').composite;
35
42
  if (mode === 'session' || mode === 'folder' || mode === 'path') {
@@ -42,14 +49,26 @@ const plugin = {
42
49
  const dangerous = settings.get('dangerouslySkipPermissions')
43
50
  .composite;
44
51
  widget.setDangerouslySkipPermissions(!!dangerous);
52
+ const sidebar = settings.get('sidebar').composite;
53
+ if ((sidebar === 'left' || sidebar === 'right') &&
54
+ sidebar !== currentSidebar) {
55
+ currentSidebar = sidebar;
56
+ // Lumino re-parents the widget cleanly when add() is called
57
+ // against a different area.
58
+ labShell.add(widget, sidebar, { rank: 600 });
59
+ }
45
60
  };
46
61
  apply();
47
62
  settings.changed.connect(apply);
48
63
  }
49
64
  catch (err) {
50
65
  console.warn('[jupyterlab_claude_code_extension] failed to load settings; using defaults', err);
66
+ labShell.add(widget, currentSidebar, { rank: 600 });
51
67
  }
52
68
  }
69
+ else {
70
+ labShell.add(widget, currentSidebar, { rank: 600 });
71
+ }
53
72
  // Register with the layout restorer so JL remembers whether the panel
54
73
  // was active/visible across browser reloads and restarts.
55
74
  if (restorer) {
package/lib/types.d.ts CHANGED
@@ -3,6 +3,7 @@ export interface ISession {
3
3
  encoded_path: string;
4
4
  session_id: string;
5
5
  name: string;
6
+ name_source: 'rename' | 'basename';
6
7
  summary: string;
7
8
  first_prompt: string;
8
9
  message_count: number;
package/lib/widget.d.ts CHANGED
@@ -49,7 +49,14 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
49
49
  * handled separately in ``_disambiguate``). */
50
50
  private _displayName;
51
51
  private _basename;
52
- /** Walk path tails until every name in a colliding group is unique. */
52
+ /** Walk path tails until every name in a colliding group is unique.
53
+ *
54
+ * User-set rename names (``name_source === 'rename'``) survive
55
+ * disambiguation untouched: a Claude ``/rename`` should never be rolled
56
+ * back to a path tail just because some other row happens to share its
57
+ * folder basename. Basename-derived rows in the same group get the path
58
+ * tail suffix, picked so it stays distinct from every rename label too.
59
+ */
53
60
  private _disambiguate;
54
61
  private _render;
55
62
  private _renderSection;
package/lib/widget.js CHANGED
@@ -8,7 +8,7 @@ const DEFAULT_RECENT_LIMIT = 10;
8
8
  const EXPANDED_STORAGE_KEY = 'jupyterlab_claude_code_extension:expanded';
9
9
  const DEFAULT_PRESENTATION_MODE = 'session';
10
10
  const SECTION_LABELS = {
11
- favourites: 'Favourites',
11
+ favourites: 'Favorites',
12
12
  recent: 'Recent',
13
13
  all: 'All'
14
14
  };
@@ -472,7 +472,14 @@ export class ClaudeCodeSessionsWidget extends Widget {
472
472
  const parts = p.split('/').filter(Boolean);
473
473
  return parts[parts.length - 1] || '';
474
474
  }
475
- /** Walk path tails until every name in a colliding group is unique. */
475
+ /** Walk path tails until every name in a colliding group is unique.
476
+ *
477
+ * User-set rename names (``name_source === 'rename'``) survive
478
+ * disambiguation untouched: a Claude ``/rename`` should never be rolled
479
+ * back to a path tail just because some other row happens to share its
480
+ * folder basename. Basename-derived rows in the same group get the path
481
+ * tail suffix, picked so it stays distinct from every rename label too.
482
+ */
476
483
  _disambiguate(rows) {
477
484
  var _a;
478
485
  const out = new Map();
@@ -486,20 +493,38 @@ export class ClaudeCodeSessionsWidget extends Widget {
486
493
  out.set(group[0].project_path, name);
487
494
  continue;
488
495
  }
489
- const segs = group.map(r => r.project_path.split('/').filter(Boolean));
496
+ const renames = group.filter(r => r.name_source === 'rename');
497
+ const basenames = group.filter(r => r.name_source !== 'rename');
498
+ // Rename rows win unchanged. The ``taken`` set guards basename
499
+ // disambiguation from colliding back into a rename's label.
500
+ const taken = new Set();
501
+ for (const r of renames) {
502
+ out.set(r.project_path, r.name);
503
+ taken.add(r.name);
504
+ }
505
+ if (basenames.length === 0) {
506
+ continue;
507
+ }
508
+ // Walk path tails for basename rows: tail must be unique among
509
+ // basenames AND not equal to any rename label already taken.
510
+ const segs = basenames.map(r => r.project_path.split('/').filter(Boolean));
490
511
  const max = Math.max(...segs.map(s => s.length));
491
512
  let depth = 1;
513
+ let resolved = false;
492
514
  while (depth <= max) {
493
515
  const tails = segs.map(s => s.slice(-depth).join('/'));
494
- if (new Set(tails).size === tails.length) {
495
- group.forEach((r, i) => out.set(r.project_path, tails[i]));
516
+ const unique = new Set(tails).size === tails.length;
517
+ const noConflict = tails.every(t => !taken.has(t));
518
+ if (unique && noConflict) {
519
+ basenames.forEach((r, i) => out.set(r.project_path, tails[i]));
520
+ resolved = true;
496
521
  break;
497
522
  }
498
523
  depth += 1;
499
524
  }
500
- if (!out.has(group[0].project_path)) {
525
+ if (!resolved) {
501
526
  // Fallback to absolute path
502
- group.forEach(r => out.set(r.project_path, r.project_path));
527
+ basenames.forEach(r => out.set(r.project_path, r.project_path));
503
528
  }
504
529
  }
505
530
  return out;
@@ -582,7 +607,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
582
607
  const empty = document.createElement('div');
583
608
  empty.className = 'jp-ClaudeSessionsPanel-emptySection';
584
609
  empty.textContent =
585
- key === 'favourites' ? 'No favourites yet.' : 'Empty.';
610
+ key === 'favourites' ? 'No favorites yet.' : 'Empty.';
586
611
  list.appendChild(empty);
587
612
  }
588
613
  else {
@@ -623,12 +648,12 @@ export class ClaudeCodeSessionsWidget extends Widget {
623
648
  name.className = 'jp-ClaudeSessionsPanel-name';
624
649
  name.textContent = this._lookupName(session);
625
650
  row.appendChild(name);
626
- // No star in the Favourites section - every row there is a favourite
651
+ // No star in the Favorites section - every row there is a favorite
627
652
  // by definition; stars are an indicator only useful in Recent/All.
628
653
  if (session.favourite && sectionKey !== 'favourites') {
629
654
  const star = document.createElement('span');
630
655
  star.className = 'jp-ClaudeSessionsPanel-favStar';
631
- star.title = 'Favourite';
656
+ star.title = 'Favorite';
632
657
  starFilledIcon.element({ container: star });
633
658
  row.appendChild(star);
634
659
  }
@@ -726,8 +751,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
726
751
  label: () => {
727
752
  var _a;
728
753
  return ((_a = this._activeSession) === null || _a === void 0 ? void 0 : _a.favourite)
729
- ? 'Remove from Favourites'
730
- : 'Add to Favourites';
754
+ ? 'Remove from Favorites'
755
+ : 'Add to Favorites';
731
756
  },
732
757
  execute: () => {
733
758
  if (this._activeSession) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_claude_code_extension",
3
- "version": "1.0.50",
3
+ "version": "1.0.54",
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",
@@ -5,6 +5,7 @@ const session = (over: Partial<ISession> = {}): ISession => ({
5
5
  encoded_path: '-p',
6
6
  session_id: 'sid',
7
7
  name: 'P',
8
+ name_source: 'basename',
8
9
  summary: '',
9
10
  first_prompt: '',
10
11
  message_count: 0,
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ const WIDGET_ID = 'jupyterlab-claude-code-extension';
17
17
  const plugin: JupyterFrontEndPlugin<void> = {
18
18
  id: PLUGIN_ID,
19
19
  description:
20
- 'Side panel listing Claude Code sessions per project folder, with remote-control indicator, favourites, and one-click resume in a terminal.',
20
+ 'Side panel listing Claude Code sessions per project folder, with remote-control indicator, favorites, and one-click resume in a terminal.',
21
21
  autoStart: true,
22
22
  requires: [ILabShell],
23
23
  optional: [ILayoutRestorer, ISettingRegistry, ITerminalTracker],
@@ -53,11 +53,20 @@ const plugin: JupyterFrontEndPlugin<void> = {
53
53
  status.root_dir || '',
54
54
  terminalTracker
55
55
  );
56
- labShell.add(widget, 'left', { rank: 600 });
56
+
57
+ // Read the sidebar setting before docking so we add the widget to the
58
+ // user's preferred side on first paint. Default to left.
59
+ let currentSidebar: 'left' | 'right' = 'left';
57
60
 
58
61
  if (settingRegistry) {
59
62
  try {
60
63
  const settings = await settingRegistry.load(PLUGIN_ID);
64
+ const initialSidebar = settings.get('sidebar').composite as string;
65
+ if (initialSidebar === 'left' || initialSidebar === 'right') {
66
+ currentSidebar = initialSidebar;
67
+ }
68
+ labShell.add(widget, currentSidebar, { rank: 600 });
69
+
61
70
  const apply = (): void => {
62
71
  const mode = settings.get('presentationMode').composite as string;
63
72
  if (mode === 'session' || mode === 'folder' || mode === 'path') {
@@ -70,6 +79,16 @@ const plugin: JupyterFrontEndPlugin<void> = {
70
79
  const dangerous = settings.get('dangerouslySkipPermissions')
71
80
  .composite as boolean;
72
81
  widget.setDangerouslySkipPermissions(!!dangerous);
82
+ const sidebar = settings.get('sidebar').composite as string;
83
+ if (
84
+ (sidebar === 'left' || sidebar === 'right') &&
85
+ sidebar !== currentSidebar
86
+ ) {
87
+ currentSidebar = sidebar;
88
+ // Lumino re-parents the widget cleanly when add() is called
89
+ // against a different area.
90
+ labShell.add(widget, sidebar, { rank: 600 });
91
+ }
73
92
  };
74
93
  apply();
75
94
  settings.changed.connect(apply);
@@ -78,7 +97,10 @@ const plugin: JupyterFrontEndPlugin<void> = {
78
97
  '[jupyterlab_claude_code_extension] failed to load settings; using defaults',
79
98
  err
80
99
  );
100
+ labShell.add(widget, currentSidebar, { rank: 600 });
81
101
  }
102
+ } else {
103
+ labShell.add(widget, currentSidebar, { rank: 600 });
82
104
  }
83
105
 
84
106
  // Register with the layout restorer so JL remembers whether the panel
package/src/types.ts CHANGED
@@ -3,6 +3,7 @@ export interface ISession {
3
3
  encoded_path: string;
4
4
  session_id: string;
5
5
  name: string;
6
+ name_source: 'rename' | 'basename';
6
7
  summary: string;
7
8
  first_prompt: string;
8
9
  message_count: number;
package/src/widget.ts CHANGED
@@ -33,7 +33,7 @@ export type PresentationMode = 'session' | 'folder' | 'path';
33
33
  const DEFAULT_PRESENTATION_MODE: PresentationMode = 'session';
34
34
 
35
35
  const SECTION_LABELS: Record<SectionKey, string> = {
36
- favourites: 'Favourites',
36
+ favourites: 'Favorites',
37
37
  recent: 'Recent',
38
38
  all: 'All'
39
39
  };
@@ -566,7 +566,14 @@ export class ClaudeCodeSessionsWidget extends Widget {
566
566
  return parts[parts.length - 1] || '';
567
567
  }
568
568
 
569
- /** Walk path tails until every name in a colliding group is unique. */
569
+ /** Walk path tails until every name in a colliding group is unique.
570
+ *
571
+ * User-set rename names (``name_source === 'rename'``) survive
572
+ * disambiguation untouched: a Claude ``/rename`` should never be rolled
573
+ * back to a path tail just because some other row happens to share its
574
+ * folder basename. Basename-derived rows in the same group get the path
575
+ * tail suffix, picked so it stays distinct from every rename label too.
576
+ */
570
577
  private _disambiguate(rows: ISession[]): Map<string, string> {
571
578
  const out = new Map<string, string>();
572
579
  const groups = new Map<string, ISession[]>();
@@ -579,20 +586,43 @@ export class ClaudeCodeSessionsWidget extends Widget {
579
586
  out.set(group[0].project_path, name);
580
587
  continue;
581
588
  }
582
- const segs = group.map(r => r.project_path.split('/').filter(Boolean));
589
+
590
+ const renames = group.filter(r => r.name_source === 'rename');
591
+ const basenames = group.filter(r => r.name_source !== 'rename');
592
+
593
+ // Rename rows win unchanged. The ``taken`` set guards basename
594
+ // disambiguation from colliding back into a rename's label.
595
+ const taken = new Set<string>();
596
+ for (const r of renames) {
597
+ out.set(r.project_path, r.name);
598
+ taken.add(r.name);
599
+ }
600
+ if (basenames.length === 0) {
601
+ continue;
602
+ }
603
+
604
+ // Walk path tails for basename rows: tail must be unique among
605
+ // basenames AND not equal to any rename label already taken.
606
+ const segs = basenames.map(r =>
607
+ r.project_path.split('/').filter(Boolean)
608
+ );
583
609
  const max = Math.max(...segs.map(s => s.length));
584
610
  let depth = 1;
611
+ let resolved = false;
585
612
  while (depth <= max) {
586
613
  const tails = segs.map(s => s.slice(-depth).join('/'));
587
- if (new Set(tails).size === tails.length) {
588
- group.forEach((r, i) => out.set(r.project_path, tails[i]));
614
+ const unique = new Set(tails).size === tails.length;
615
+ const noConflict = tails.every(t => !taken.has(t));
616
+ if (unique && noConflict) {
617
+ basenames.forEach((r, i) => out.set(r.project_path, tails[i]));
618
+ resolved = true;
589
619
  break;
590
620
  }
591
621
  depth += 1;
592
622
  }
593
- if (!out.has(group[0].project_path)) {
623
+ if (!resolved) {
594
624
  // Fallback to absolute path
595
- group.forEach(r => out.set(r.project_path, r.project_path));
625
+ basenames.forEach(r => out.set(r.project_path, r.project_path));
596
626
  }
597
627
  }
598
628
  return out;
@@ -694,7 +724,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
694
724
  const empty = document.createElement('div');
695
725
  empty.className = 'jp-ClaudeSessionsPanel-emptySection';
696
726
  empty.textContent =
697
- key === 'favourites' ? 'No favourites yet.' : 'Empty.';
727
+ key === 'favourites' ? 'No favorites yet.' : 'Empty.';
698
728
  list.appendChild(empty);
699
729
  } else {
700
730
  for (const item of items) {
@@ -741,12 +771,12 @@ export class ClaudeCodeSessionsWidget extends Widget {
741
771
  name.textContent = this._lookupName(session);
742
772
  row.appendChild(name);
743
773
 
744
- // No star in the Favourites section - every row there is a favourite
774
+ // No star in the Favorites section - every row there is a favorite
745
775
  // by definition; stars are an indicator only useful in Recent/All.
746
776
  if (session.favourite && sectionKey !== 'favourites') {
747
777
  const star = document.createElement('span');
748
778
  star.className = 'jp-ClaudeSessionsPanel-favStar';
749
- star.title = 'Favourite';
779
+ star.title = 'Favorite';
750
780
  starFilledIcon.element({ container: star });
751
781
  row.appendChild(star);
752
782
  }
@@ -853,8 +883,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
853
883
  this._commands.addCommand('claude-code-sessions:toggle-favourite', {
854
884
  label: () =>
855
885
  this._activeSession?.favourite
856
- ? 'Remove from Favourites'
857
- : 'Add to Favourites',
886
+ ? 'Remove from Favorites'
887
+ : 'Add to Favorites',
858
888
  execute: () => {
859
889
  if (this._activeSession) {
860
890
  void this._toggleFavourite(this._activeSession);
package/style/base.css CHANGED
@@ -24,13 +24,16 @@
24
24
  min-height: 24px;
25
25
  }
26
26
 
27
+ /* The panel title and section headers mirror the file browser's headers
28
+ (jp-Favorites-header / jp-FileBrowser-header) so the sidebar reads
29
+ uniformly: 600 weight, uppercase, ui-font-size0, 1px tracking. */
27
30
  .jp-ClaudeSessionsPanel-title {
28
31
  flex: 1 1 auto;
29
32
  font-weight: 600;
30
- letter-spacing: 0.04em;
33
+ letter-spacing: 1px;
31
34
  text-transform: uppercase;
32
35
  font-size: var(--jp-ui-font-size0);
33
- color: var(--jp-ui-font-color2);
36
+ color: var(--jp-ui-font-color1);
34
37
  }
35
38
 
36
39
  .jp-ClaudeSessionsPanel-iconButton {
@@ -123,7 +126,7 @@
123
126
  font-family: inherit;
124
127
  font-size: var(--jp-ui-font-size0);
125
128
  font-weight: 600;
126
- letter-spacing: 0.04em;
129
+ letter-spacing: 1px;
127
130
  text-transform: uppercase;
128
131
  }
129
132