jupyterlab_claude_code_extension 1.1.25 → 1.1.27

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/lib/icons.d.ts CHANGED
@@ -4,4 +4,6 @@ export declare const starFilledIcon: LabIcon;
4
4
  export declare const refreshIcon: LabIcon;
5
5
  export declare const removeIcon: LabIcon;
6
6
  export declare const shieldIcon: LabIcon;
7
+ export declare const addIcon: LabIcon;
8
+ export declare const addShieldIcon: LabIcon;
7
9
  export declare const filterIcon: LabIcon;
package/lib/icons.js CHANGED
@@ -49,6 +49,28 @@ export const shieldIcon = new LabIcon({
49
49
  name: 'jupyterlab_claude_code_extension:shield',
50
50
  svgstr: shieldSvgStr
51
51
  });
52
+ // Material "add" plus, same 16px/jp-icon3 treatment as the other header
53
+ // icons.
54
+ const addSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
55
+ <path class="jp-icon3" fill="#616161" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
56
+ </svg>`;
57
+ export const addIcon = new LabIcon({
58
+ name: 'jupyterlab_claude_code_extension:add',
59
+ svgstr: addSvgStr
60
+ });
61
+ // Plus with a small solid shield in the lower-right corner - the
62
+ // skip-permissions variant of the "new session" button, echoing the
63
+ // shield used on the context menu's "Resume (Skip Permissions)".
64
+ const addShieldSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
65
+ <g class="jp-icon3" fill="#616161">
66
+ <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" transform="translate(-2,-2) scale(0.83)"/>
67
+ <path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z" transform="translate(13,12) scale(0.46)"/>
68
+ </g>
69
+ </svg>`;
70
+ export const addShieldIcon = new LabIcon({
71
+ name: 'jupyterlab_claude_code_extension:add-shield',
72
+ svgstr: addShieldSvgStr
73
+ });
52
74
  // Funnel copied verbatim from @jupyterlab/ui-components'
53
75
  // `search/filter.svg` - the same image the file browser's filter
54
76
  // toggle uses. The `class="jp-icon3"` lets JupyterLab's theme drive
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ILabShell, ILayoutRestorer } from '@jupyterlab/application';
2
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
2
3
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
3
4
  import { ITerminalTracker } from '@jupyterlab/terminal';
4
5
  import { requestAPI } from './request';
@@ -10,8 +11,13 @@ const plugin = {
10
11
  description: 'Side panel listing Claude Code sessions per project folder, with remote-control indicator, favorites, and one-click resume in a terminal.',
11
12
  autoStart: true,
12
13
  requires: [ILabShell],
13
- optional: [ILayoutRestorer, ISettingRegistry, ITerminalTracker],
14
- activate: async (app, labShell, restorer, settingRegistry, terminalTracker) => {
14
+ optional: [
15
+ ILayoutRestorer,
16
+ ISettingRegistry,
17
+ ITerminalTracker,
18
+ IDefaultFileBrowser
19
+ ],
20
+ activate: async (app, labShell, restorer, settingRegistry, terminalTracker, fileBrowser) => {
15
21
  const settings = app.serviceManager.serverSettings;
16
22
  let status;
17
23
  try {
@@ -25,7 +31,7 @@ const plugin = {
25
31
  console.info('[jupyterlab_claude_code_extension] `claude` binary not found on PATH; panel disabled.');
26
32
  return;
27
33
  }
28
- const widget = new ClaudeCodeSessionsWidget(app, status.root_dir || '', terminalTracker);
34
+ const widget = new ClaudeCodeSessionsWidget(app, status.root_dir || '', terminalTracker, fileBrowser);
29
35
  // Read the sidebar setting before docking so we add the widget to the
30
36
  // user's preferred side on first paint. Default to left.
31
37
  let currentSidebar = 'left';
package/lib/types.d.ts CHANGED
@@ -44,7 +44,8 @@ export interface ICleanupResponse {
44
44
  }
45
45
  export interface ILaunchTerminalRequest {
46
46
  project_path: string;
47
- session_id: string;
47
+ /** Omit to start a brand-new claude session instead of resuming one. */
48
+ session_id?: string;
48
49
  dangerously_skip_permissions?: boolean;
49
50
  }
50
51
  export interface ILaunchTerminalResponse {
package/lib/widget.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { JupyterFrontEnd } from '@jupyterlab/application';
2
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
2
3
  import { ITerminalTracker } from '@jupyterlab/terminal';
3
4
  import { Widget } from '@lumino/widgets';
4
5
  import { Message } from '@lumino/messaging';
5
6
  export type PresentationMode = 'folder' | 'path';
6
7
  export declare class ClaudeCodeSessionsWidget extends Widget {
7
- constructor(app: JupyterFrontEnd, rootDir: string, terminalTracker?: ITerminalTracker | null);
8
+ constructor(app: JupyterFrontEnd, rootDir: string, terminalTracker?: ITerminalTracker | null, fileBrowser?: IDefaultFileBrowser | null);
8
9
  refresh(): void;
9
10
  /** Choose how rows are labelled: by session name, folder name, or path. */
10
11
  setPresentationMode(mode: PresentationMode): void;
@@ -44,6 +45,15 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
44
45
  private _cleanupParallel;
45
46
  private _resumeInTerminal;
46
47
  private _doResumeInTerminal;
48
+ /** Absolute path of the file browser's current folder; falls back to the
49
+ * server root when no file browser is available. */
50
+ private _currentFolder;
51
+ /** Start a brand-new claude session in the file browser's current folder.
52
+ * Same launch path as resuming (claude is the pty's only process via the
53
+ * launch-terminal endpoint) - just without --resume, and always a fresh
54
+ * terminal since there is no existing session to reuse.
55
+ */
56
+ private _newSession;
47
57
  /**
48
58
  * Bring a terminal tab to the front AND hand it keyboard focus, so the
49
59
  * user can start typing without an extra click. `activateById` only
@@ -102,6 +112,7 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
102
112
  private _pollHandle;
103
113
  private readonly _removingPaths;
104
114
  private readonly _terminalTracker;
115
+ private readonly _fileBrowser;
105
116
  private readonly _terminalsByPath;
106
117
  private readonly _pendingByPath;
107
118
  private readonly _rootDir;
package/lib/widget.js CHANGED
@@ -3,7 +3,7 @@ import { folderIcon, terminalIcon } from '@jupyterlab/ui-components';
3
3
  import { CommandRegistry } from '@lumino/commands';
4
4
  import { Menu, Widget } from '@lumino/widgets';
5
5
  import { requestAPI } from './request';
6
- import { claudeIcon, filterIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon } from './icons';
6
+ import { addIcon, addShieldIcon, claudeIcon, filterIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon } from './icons';
7
7
  const POLL_INTERVAL_MS = 30000;
8
8
  const DEFAULT_RECENT_LIMIT = 10;
9
9
  const EXPANDED_STORAGE_KEY = 'jupyterlab_claude_code_extension:expanded';
@@ -56,7 +56,7 @@ catch (_err) {
56
56
  // ignore
57
57
  }
58
58
  export class ClaudeCodeSessionsWidget extends Widget {
59
- constructor(app, rootDir, terminalTracker = null) {
59
+ constructor(app, rootDir, terminalTracker = null, fileBrowser = null) {
60
60
  super();
61
61
  this._refreshBtn = null;
62
62
  this._filterBtn = null;
@@ -78,6 +78,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
78
78
  this._serverSettings = app.serviceManager.serverSettings;
79
79
  this._rootDir = rootDir.replace(/\/+$/, '');
80
80
  this._terminalTracker = terminalTracker;
81
+ this._fileBrowser = fileBrowser;
81
82
  this.id = 'jupyterlab-claude-code-extension';
82
83
  this.title.icon = claudeIcon;
83
84
  this.title.caption = 'Claude Code Sessions';
@@ -139,6 +140,19 @@ export class ClaudeCodeSessionsWidget extends Widget {
139
140
  title.className = 'jp-ClaudeSessionsPanel-title';
140
141
  title.textContent = 'Claude Code Sessions';
141
142
  header.appendChild(title);
143
+ const newBtn = document.createElement('button');
144
+ newBtn.className = 'jp-ClaudeSessionsPanel-iconButton';
145
+ newBtn.title = 'New Claude session in the current folder';
146
+ addIcon.element({ container: newBtn });
147
+ newBtn.addEventListener('click', () => void this._newSession(false));
148
+ header.appendChild(newBtn);
149
+ const newSkipBtn = document.createElement('button');
150
+ newSkipBtn.className = 'jp-ClaudeSessionsPanel-iconButton';
151
+ newSkipBtn.title =
152
+ 'New Claude session in the current folder (Skip Permissions)';
153
+ addShieldIcon.element({ container: newSkipBtn });
154
+ newSkipBtn.addEventListener('click', () => void this._newSession(true));
155
+ header.appendChild(newSkipBtn);
142
156
  const filterBtn = document.createElement('button');
143
157
  filterBtn.className = 'jp-ClaudeSessionsPanel-iconButton';
144
158
  filterBtn.title = 'Filter sessions';
@@ -464,6 +478,53 @@ export class ClaudeCodeSessionsWidget extends Widget {
464
478
  });
465
479
  }
466
480
  }
481
+ /** Absolute path of the file browser's current folder; falls back to the
482
+ * server root when no file browser is available. */
483
+ _currentFolder() {
484
+ var _a, _b, _c;
485
+ const rel = ((_c = (_b = (_a = this._fileBrowser) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : '').replace(/^\/+/, '');
486
+ return rel ? `${this._rootDir}/${rel}` : this._rootDir;
487
+ }
488
+ /** Start a brand-new claude session in the file browser's current folder.
489
+ * Same launch path as resuming (claude is the pty's only process via the
490
+ * launch-terminal endpoint) - just without --resume, and always a fresh
491
+ * terminal since there is no existing session to reuse.
492
+ */
493
+ async _newSession(forceDangerous) {
494
+ const projectPath = this._currentFolder();
495
+ if (!projectPath) {
496
+ return;
497
+ }
498
+ const spinner = this._showLaunchSpinner();
499
+ try {
500
+ const launched = await requestAPI('launch-terminal', this._serverSettings, {
501
+ method: 'POST',
502
+ body: JSON.stringify({
503
+ project_path: projectPath,
504
+ dangerously_skip_permissions: forceDangerous || this._dangerouslySkip
505
+ })
506
+ });
507
+ const widget = await this._app.commands.execute('terminal:open', {
508
+ name: launched.terminal_name
509
+ });
510
+ if (widget === null || widget === void 0 ? void 0 : widget.id) {
511
+ this._terminalsByPath.set(projectPath, widget);
512
+ this._wireTerminalDisposal(projectPath, widget);
513
+ this._focusTerminal(widget);
514
+ }
515
+ }
516
+ catch (err) {
517
+ this._showError(err);
518
+ }
519
+ finally {
520
+ spinner.dispose();
521
+ // The new session creates a project dir under ~/.claude - refresh so
522
+ // its row (and remote-control state) appears without waiting a poll.
523
+ void this._fetch().catch(() => {
524
+ /* a poll tick will retry; nothing actionable here */
525
+ });
526
+ }
527
+ }
467
528
  /**
468
529
  * Bring a terminal tab to the front AND hand it keyboard focus, so the
469
530
  * user can start typing without an extra click. `activateById` only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_claude_code_extension",
3
- "version": "1.1.25",
3
+ "version": "1.1.27",
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",
@@ -58,6 +58,7 @@
58
58
  "dependencies": {
59
59
  "@jupyterlab/apputils": "^4.0.0",
60
60
  "@jupyterlab/coreutils": "^6.0.0",
61
+ "@jupyterlab/filebrowser": "^4.0.0",
61
62
  "@jupyterlab/services": "^7.0.0",
62
63
  "@jupyterlab/settingregistry": "^4.0.0",
63
64
  "@jupyterlab/terminal": "^4.0.0",
package/src/icons.ts CHANGED
@@ -60,6 +60,32 @@ export const shieldIcon = new LabIcon({
60
60
  svgstr: shieldSvgStr
61
61
  });
62
62
 
63
+ // Material "add" plus, same 16px/jp-icon3 treatment as the other header
64
+ // icons.
65
+ const addSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
66
+ <path class="jp-icon3" fill="#616161" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
67
+ </svg>`;
68
+
69
+ export const addIcon = new LabIcon({
70
+ name: 'jupyterlab_claude_code_extension:add',
71
+ svgstr: addSvgStr
72
+ });
73
+
74
+ // Plus with a small solid shield in the lower-right corner - the
75
+ // skip-permissions variant of the "new session" button, echoing the
76
+ // shield used on the context menu's "Resume (Skip Permissions)".
77
+ const addShieldSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
78
+ <g class="jp-icon3" fill="#616161">
79
+ <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" transform="translate(-2,-2) scale(0.83)"/>
80
+ <path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z" transform="translate(13,12) scale(0.46)"/>
81
+ </g>
82
+ </svg>`;
83
+
84
+ export const addShieldIcon = new LabIcon({
85
+ name: 'jupyterlab_claude_code_extension:add-shield',
86
+ svgstr: addShieldSvgStr
87
+ });
88
+
63
89
  // Funnel copied verbatim from @jupyterlab/ui-components'
64
90
  // `search/filter.svg` - the same image the file browser's filter
65
91
  // toggle uses. The `class="jp-icon3"` lets JupyterLab's theme drive
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  JupyterFrontEnd,
5
5
  JupyterFrontEndPlugin
6
6
  } from '@jupyterlab/application';
7
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
7
8
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
8
9
  import { ITerminalTracker } from '@jupyterlab/terminal';
9
10
 
@@ -20,13 +21,19 @@ const plugin: JupyterFrontEndPlugin<void> = {
20
21
  'Side panel listing Claude Code sessions per project folder, with remote-control indicator, favorites, and one-click resume in a terminal.',
21
22
  autoStart: true,
22
23
  requires: [ILabShell],
23
- optional: [ILayoutRestorer, ISettingRegistry, ITerminalTracker],
24
+ optional: [
25
+ ILayoutRestorer,
26
+ ISettingRegistry,
27
+ ITerminalTracker,
28
+ IDefaultFileBrowser
29
+ ],
24
30
  activate: async (
25
31
  app: JupyterFrontEnd,
26
32
  labShell: ILabShell,
27
33
  restorer: ILayoutRestorer | null,
28
34
  settingRegistry: ISettingRegistry | null,
29
- terminalTracker: ITerminalTracker | null
35
+ terminalTracker: ITerminalTracker | null,
36
+ fileBrowser: IDefaultFileBrowser | null
30
37
  ) => {
31
38
  const settings = app.serviceManager.serverSettings;
32
39
 
@@ -51,7 +58,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
51
58
  const widget = new ClaudeCodeSessionsWidget(
52
59
  app,
53
60
  status.root_dir || '',
54
- terminalTracker
61
+ terminalTracker,
62
+ fileBrowser
55
63
  );
56
64
 
57
65
  // Read the sidebar setting before docking so we add the widget to the
package/src/types.ts CHANGED
@@ -53,7 +53,8 @@ export interface ICleanupResponse {
53
53
 
54
54
  export interface ILaunchTerminalRequest {
55
55
  project_path: string;
56
- session_id: string;
56
+ /** Omit to start a brand-new claude session instead of resuming one. */
57
+ session_id?: string;
57
58
  dangerously_skip_permissions?: boolean;
58
59
  }
59
60
 
package/src/widget.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  showDialog
7
7
  } from '@jupyterlab/apputils';
8
8
  import { ServerConnection } from '@jupyterlab/services';
9
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
9
10
  import { ITerminalTracker } from '@jupyterlab/terminal';
10
11
  import { folderIcon, terminalIcon } from '@jupyterlab/ui-components';
11
12
  import { CommandRegistry } from '@lumino/commands';
@@ -14,6 +15,8 @@ import { Message } from '@lumino/messaging';
14
15
 
15
16
  import { requestAPI } from './request';
16
17
  import {
18
+ addIcon,
19
+ addShieldIcon,
17
20
  claudeIcon,
18
21
  filterIcon,
19
22
  refreshIcon,
@@ -101,13 +104,15 @@ export class ClaudeCodeSessionsWidget extends Widget {
101
104
  constructor(
102
105
  app: JupyterFrontEnd,
103
106
  rootDir: string,
104
- terminalTracker: ITerminalTracker | null = null
107
+ terminalTracker: ITerminalTracker | null = null,
108
+ fileBrowser: IDefaultFileBrowser | null = null
105
109
  ) {
106
110
  super();
107
111
  this._app = app;
108
112
  this._serverSettings = app.serviceManager.serverSettings;
109
113
  this._rootDir = rootDir.replace(/\/+$/, '');
110
114
  this._terminalTracker = terminalTracker;
115
+ this._fileBrowser = fileBrowser;
111
116
 
112
117
  this.id = 'jupyterlab-claude-code-extension';
113
118
  this.title.icon = claudeIcon;
@@ -185,6 +190,21 @@ export class ClaudeCodeSessionsWidget extends Widget {
185
190
  title.textContent = 'Claude Code Sessions';
186
191
  header.appendChild(title);
187
192
 
193
+ const newBtn = document.createElement('button');
194
+ newBtn.className = 'jp-ClaudeSessionsPanel-iconButton';
195
+ newBtn.title = 'New Claude session in the current folder';
196
+ addIcon.element({ container: newBtn });
197
+ newBtn.addEventListener('click', () => void this._newSession(false));
198
+ header.appendChild(newBtn);
199
+
200
+ const newSkipBtn = document.createElement('button');
201
+ newSkipBtn.className = 'jp-ClaudeSessionsPanel-iconButton';
202
+ newSkipBtn.title =
203
+ 'New Claude session in the current folder (Skip Permissions)';
204
+ addShieldIcon.element({ container: newSkipBtn });
205
+ newSkipBtn.addEventListener('click', () => void this._newSession(true));
206
+ header.appendChild(newSkipBtn);
207
+
188
208
  const filterBtn = document.createElement('button');
189
209
  filterBtn.className = 'jp-ClaudeSessionsPanel-iconButton';
190
210
  filterBtn.title = 'Filter sessions';
@@ -568,6 +588,57 @@ export class ClaudeCodeSessionsWidget extends Widget {
568
588
  }
569
589
  }
570
590
 
591
+ /** Absolute path of the file browser's current folder; falls back to the
592
+ * server root when no file browser is available. */
593
+ private _currentFolder(): string {
594
+ const rel = (this._fileBrowser?.model?.path ?? '').replace(/^\/+/, '');
595
+ return rel ? `${this._rootDir}/${rel}` : this._rootDir;
596
+ }
597
+
598
+ /** Start a brand-new claude session in the file browser's current folder.
599
+ * Same launch path as resuming (claude is the pty's only process via the
600
+ * launch-terminal endpoint) - just without --resume, and always a fresh
601
+ * terminal since there is no existing session to reuse.
602
+ */
603
+ private async _newSession(forceDangerous: boolean): Promise<void> {
604
+ const projectPath = this._currentFolder();
605
+ if (!projectPath) {
606
+ return;
607
+ }
608
+ const spinner = this._showLaunchSpinner();
609
+ try {
610
+ const launched = await requestAPI<ILaunchTerminalResponse>(
611
+ 'launch-terminal',
612
+ this._serverSettings,
613
+ {
614
+ method: 'POST',
615
+ body: JSON.stringify({
616
+ project_path: projectPath,
617
+ dangerously_skip_permissions:
618
+ forceDangerous || this._dangerouslySkip
619
+ })
620
+ }
621
+ );
622
+ const widget: any = await this._app.commands.execute('terminal:open', {
623
+ name: launched.terminal_name
624
+ });
625
+ if (widget?.id) {
626
+ this._terminalsByPath.set(projectPath, widget);
627
+ this._wireTerminalDisposal(projectPath, widget);
628
+ this._focusTerminal(widget);
629
+ }
630
+ } catch (err) {
631
+ this._showError(err);
632
+ } finally {
633
+ spinner.dispose();
634
+ // The new session creates a project dir under ~/.claude - refresh so
635
+ // its row (and remote-control state) appears without waiting a poll.
636
+ void this._fetch().catch(() => {
637
+ /* a poll tick will retry; nothing actionable here */
638
+ });
639
+ }
640
+ }
641
+
571
642
  /**
572
643
  * Bring a terminal tab to the front AND hand it keyboard focus, so the
573
644
  * user can start typing without an extra click. `activateById` only
@@ -1192,6 +1263,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
1192
1263
  private _pollHandle: number | null = null;
1193
1264
  private readonly _removingPaths: Set<string> = new Set();
1194
1265
  private readonly _terminalTracker: ITerminalTracker | null;
1266
+ private readonly _fileBrowser: IDefaultFileBrowser | null;
1195
1267
  private readonly _terminalsByPath: Map<string, any> = new Map();
1196
1268
  private readonly _pendingByPath: Map<string, Promise<void>> = new Map();
1197
1269
  private readonly _rootDir: string;