jupyterlab_claude_code_extension 1.0.21 → 1.0.29

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
@@ -3,3 +3,4 @@ export declare const claudeIcon: LabIcon;
3
3
  export declare const starFilledIcon: LabIcon;
4
4
  export declare const refreshIcon: LabIcon;
5
5
  export declare const removeIcon: LabIcon;
6
+ export declare const shieldIcon: LabIcon;
package/lib/icons.js CHANGED
@@ -42,3 +42,10 @@ export const removeIcon = new LabIcon({
42
42
  name: 'jupyterlab_claude_code_extension:remove',
43
43
  svgstr: removeSvgStr
44
44
  });
45
+ const shieldSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="13" height="13">
46
+ <path class="jp-icon3" fill="#616161" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
47
+ </svg>`;
48
+ export const shieldIcon = new LabIcon({
49
+ name: 'jupyterlab_claude_code_extension:shield',
50
+ svgstr: shieldSvgStr
51
+ });
package/lib/index.js CHANGED
@@ -38,6 +38,9 @@ const plugin = {
38
38
  if (typeof limit === 'number') {
39
39
  widget.setRecentLimit(limit);
40
40
  }
41
+ const dangerous = settings.get('dangerouslySkipPermissions')
42
+ .composite;
43
+ widget.setDangerouslySkipPermissions(!!dangerous);
41
44
  };
42
45
  apply();
43
46
  settings.changed.connect(apply);
package/lib/types.d.ts CHANGED
@@ -37,6 +37,7 @@ export interface IRemoveResponse {
37
37
  export interface ILaunchTerminalRequest {
38
38
  project_path: string;
39
39
  session_id: string;
40
+ dangerously_skip_permissions?: boolean;
40
41
  }
41
42
  export interface ILaunchTerminalResponse {
42
43
  terminal_name: string;
package/lib/widget.d.ts CHANGED
@@ -9,6 +9,8 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
9
9
  setResolveSessionNames(on: boolean): void;
10
10
  /** Set how many rows the Recent section displays. */
11
11
  setRecentLimit(n: number): void;
12
+ /** Toggle the --dangerously-skip-permissions flag on launched sessions. */
13
+ setDangerouslySkipPermissions(on: boolean): void;
12
14
  protected onAfterShow(_msg: Message): void;
13
15
  protected onBeforeHide(_msg: Message): void;
14
16
  protected onCloseRequest(msg: Message): void;
@@ -19,11 +21,11 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
19
21
  * compare equal as "foobar".
20
22
  */
21
23
  private _normalize;
22
- /** Strict fuzzy match: substring on normalised strings, with up to 2%
23
- * Levenshtein tolerance for queries of 6+ chars (so the 98% threshold
24
- * only relaxes substring strictness when the query is long enough that
25
- * 2% rounds to a nonzero edit budget - effectively substring-only for
26
- * short queries).
24
+ /** Fuzzy match at a 95% threshold: substring on normalised strings,
25
+ * with up to 5% Levenshtein tolerance. For short queries the budget
26
+ * still rounds to zero so behaviour is substring-only there - the
27
+ * relaxation only kicks in for queries long enough that 5% reaches a
28
+ * full edit (10+ chars).
27
29
  */
28
30
  private _fuzzyMatch;
29
31
  private _levenshtein;
@@ -36,6 +38,7 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
36
38
  private _resumeInTerminal;
37
39
  private _doResumeInTerminal;
38
40
  private _findTerminalForCwd;
41
+ private _showCloseExistingDialog;
39
42
  private _wireTerminalDisposal;
40
43
  /** Apply the resolve-names setting + path-segment disambiguation. */
41
44
  private _displayName;
@@ -73,6 +76,7 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
73
76
  private readonly _rootDir;
74
77
  private _resolveNames;
75
78
  private _recentLimit;
79
+ private _dangerouslySkip;
76
80
  private _displayNames;
77
81
  private _filter;
78
82
  }
package/lib/widget.js CHANGED
@@ -1,7 +1,8 @@
1
+ import { Dialog, showDialog } from '@jupyterlab/apputils';
1
2
  import { CommandRegistry } from '@lumino/commands';
2
3
  import { Menu, Widget } from '@lumino/widgets';
3
4
  import { requestAPI } from './request';
4
- import { claudeIcon, refreshIcon, removeIcon, starFilledIcon } from './icons';
5
+ import { claudeIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon } from './icons';
5
6
  const POLL_INTERVAL_MS = 10000;
6
7
  const DEFAULT_RECENT_LIMIT = 10;
7
8
  const EXPANDED_STORAGE_KEY = 'jupyterlab_claude_code_extension:expanded';
@@ -66,6 +67,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
66
67
  this._pendingByPath = new Map();
67
68
  this._resolveNames = true;
68
69
  this._recentLimit = DEFAULT_RECENT_LIMIT;
70
+ this._dangerouslySkip = false;
69
71
  this._displayNames = new Map();
70
72
  this._filter = '';
71
73
  this._app = app;
@@ -103,6 +105,10 @@ export class ClaudeCodeSessionsWidget extends Widget {
103
105
  this._recentLimit = clamped;
104
106
  this._render();
105
107
  }
108
+ /** Toggle the --dangerously-skip-permissions flag on launched sessions. */
109
+ setDangerouslySkipPermissions(on) {
110
+ this._dangerouslySkip = !!on;
111
+ }
106
112
  onAfterShow(_msg) {
107
113
  this.refresh();
108
114
  this._startPolling();
@@ -163,11 +169,11 @@ export class ClaudeCodeSessionsWidget extends Widget {
163
169
  .toLowerCase()
164
170
  .replace(/[\s\-_./]+/g, '');
165
171
  }
166
- /** Strict fuzzy match: substring on normalised strings, with up to 2%
167
- * Levenshtein tolerance for queries of 6+ chars (so the 98% threshold
168
- * only relaxes substring strictness when the query is long enough that
169
- * 2% rounds to a nonzero edit budget - effectively substring-only for
170
- * short queries).
172
+ /** Fuzzy match at a 95% threshold: substring on normalised strings,
173
+ * with up to 5% Levenshtein tolerance. For short queries the budget
174
+ * still rounds to zero so behaviour is substring-only there - the
175
+ * relaxation only kicks in for queries long enough that 5% reaches a
176
+ * full edit (10+ chars).
171
177
  */
172
178
  _fuzzyMatch(haystack, needle) {
173
179
  if (!needle) {
@@ -181,7 +187,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
181
187
  if (h.includes(n)) {
182
188
  return true;
183
189
  }
184
- const tol = Math.floor(n.length * 0.02);
190
+ const tol = Math.round(n.length * 0.05);
185
191
  if (tol === 0) {
186
192
  return false;
187
193
  }
@@ -288,36 +294,47 @@ export class ClaudeCodeSessionsWidget extends Widget {
288
294
  }
289
295
  }
290
296
  // -------------------------------------------------------------- terminal
291
- async _resumeInTerminal(session) {
297
+ async _resumeInTerminal(session, forceDangerous = false) {
292
298
  // Coalesce concurrent clicks on the same row - subsequent clicks attach
293
299
  // to the in-flight promise instead of creating their own terminal.
294
300
  const inFlight = this._pendingByPath.get(session.project_path);
295
301
  if (inFlight) {
296
302
  return inFlight;
297
303
  }
298
- const promise = this._doResumeInTerminal(session).finally(() => {
304
+ const promise = this._doResumeInTerminal(session, forceDangerous).finally(() => {
299
305
  this._pendingByPath.delete(session.project_path);
300
306
  });
301
307
  this._pendingByPath.set(session.project_path, promise);
302
308
  return promise;
303
309
  }
304
- async _doResumeInTerminal(session) {
310
+ async _doResumeInTerminal(session, forceDangerous) {
305
311
  try {
306
- // 1. In-memory microcache - covers rapid-click and post-creation reuse
307
- // before the new widget propagates fully through the tracker. Cleared
308
- // on widget disposal. NOT persisted to localStorage.
312
+ // Always prefer reusing an open terminal for this project. The
313
+ // skip-permissions flag can only be applied to a fresh pty, never
314
+ // retroactively. So if the user wants dangerous mode but an open
315
+ // terminal already exists, show a modal asking them to close it
316
+ // first - we won't auto-close, won't silently reuse the wrong mode.
317
+ // 1. In-memory microcache.
309
318
  const cached = this._terminalsByPath.get(session.project_path);
310
319
  if (cached && !cached.isDisposed) {
320
+ if (forceDangerous) {
321
+ await this._showCloseExistingDialog();
322
+ this._app.shell.activateById(cached.id);
323
+ return;
324
+ }
311
325
  this._app.shell.activateById(cached.id);
312
326
  return;
313
327
  }
314
- // 2. Walk every live terminal widget JL knows about and ask the
315
- // server for the cwd of EVERY process in its pty's tree. Match if
316
- // any one of them equals project_path.
328
+ // 2. Walk every live terminal widget JL knows about.
317
329
  const found = await this._findTerminalForCwd(session.project_path);
318
330
  if (found) {
319
331
  this._terminalsByPath.set(session.project_path, found);
320
332
  this._wireTerminalDisposal(session.project_path, found);
333
+ if (forceDangerous) {
334
+ await this._showCloseExistingDialog();
335
+ this._app.shell.activateById(found.id);
336
+ return;
337
+ }
321
338
  this._app.shell.activateById(found.id);
322
339
  return;
323
340
  }
@@ -330,7 +347,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
330
347
  method: 'POST',
331
348
  body: JSON.stringify({
332
349
  project_path: session.project_path,
333
- session_id: session.session_id
350
+ session_id: session.session_id,
351
+ dangerously_skip_permissions: forceDangerous || this._dangerouslySkip
334
352
  })
335
353
  });
336
354
  const widget = await this._app.commands.execute('terminal:open', {
@@ -378,6 +396,15 @@ export class ClaudeCodeSessionsWidget extends Widget {
378
396
  }
379
397
  return null;
380
398
  }
399
+ async _showCloseExistingDialog() {
400
+ await showDialog({
401
+ title: 'Existing Claude session is running',
402
+ body: 'A terminal for this project is already open. To launch with ' +
403
+ '--dangerously-skip-permissions, close that terminal first then ' +
404
+ 'click "Resume (Skip Permissions)" again.',
405
+ buttons: [Dialog.okButton({ label: 'OK' })]
406
+ });
407
+ }
381
408
  _wireTerminalDisposal(projectPath, widget) {
382
409
  var _a;
383
410
  if (!((_a = widget === null || widget === void 0 ? void 0 : widget.disposed) === null || _a === void 0 ? void 0 : _a.connect)) {
@@ -679,6 +706,15 @@ export class ClaudeCodeSessionsWidget extends Widget {
679
706
  }
680
707
  }
681
708
  });
709
+ this._commands.addCommand('claude-code-sessions:resume-dangerous', {
710
+ label: 'Resume (Skip Permissions)',
711
+ icon: shieldIcon,
712
+ execute: () => {
713
+ if (this._activeSession) {
714
+ void this._resumeInTerminal(this._activeSession, true);
715
+ }
716
+ }
717
+ });
682
718
  this._commands.addCommand('claude-code-sessions:remove', {
683
719
  label: 'Remove from Claude',
684
720
  icon: removeIcon,
@@ -691,6 +727,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
691
727
  this._contextMenu = new Menu({ commands: this._commands });
692
728
  this._contextMenu.addClass('jp-ClaudeSessionsContextMenu');
693
729
  this._contextMenu.addItem({ command: 'claude-code-sessions:resume' });
730
+ this._contextMenu.addItem({
731
+ command: 'claude-code-sessions:resume-dangerous'
732
+ });
694
733
  this._contextMenu.addItem({
695
734
  command: 'claude-code-sessions:toggle-favourite'
696
735
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_claude_code_extension",
3
- "version": "1.0.21",
3
+ "version": "1.0.29",
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/icons.ts CHANGED
@@ -50,3 +50,12 @@ export const removeIcon = new LabIcon({
50
50
  name: 'jupyterlab_claude_code_extension:remove',
51
51
  svgstr: removeSvgStr
52
52
  });
53
+
54
+ const shieldSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="13" height="13">
55
+ <path class="jp-icon3" fill="#616161" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
56
+ </svg>`;
57
+
58
+ export const shieldIcon = new LabIcon({
59
+ name: 'jupyterlab_claude_code_extension:shield',
60
+ svgstr: shieldSvgStr
61
+ });
package/src/index.ts CHANGED
@@ -66,6 +66,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
66
66
  if (typeof limit === 'number') {
67
67
  widget.setRecentLimit(limit);
68
68
  }
69
+ const dangerous = settings.get('dangerouslySkipPermissions')
70
+ .composite as boolean;
71
+ widget.setDangerouslySkipPermissions(!!dangerous);
69
72
  };
70
73
  apply();
71
74
  settings.changed.connect(apply);
package/src/types.ts CHANGED
@@ -44,6 +44,7 @@ export interface IRemoveResponse {
44
44
  export interface ILaunchTerminalRequest {
45
45
  project_path: string;
46
46
  session_id: string;
47
+ dangerously_skip_permissions?: boolean;
47
48
  }
48
49
 
49
50
  export interface ILaunchTerminalResponse {
package/src/widget.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { JupyterFrontEnd } from '@jupyterlab/application';
2
+ import { Dialog, showDialog } from '@jupyterlab/apputils';
2
3
  import { ServerConnection } from '@jupyterlab/services';
3
4
  import { ITerminalTracker } from '@jupyterlab/terminal';
4
5
  import { CommandRegistry } from '@lumino/commands';
@@ -6,7 +7,13 @@ import { Menu, Widget } from '@lumino/widgets';
6
7
  import { Message } from '@lumino/messaging';
7
8
 
8
9
  import { requestAPI } from './request';
9
- import { claudeIcon, refreshIcon, removeIcon, starFilledIcon } from './icons';
10
+ import {
11
+ claudeIcon,
12
+ refreshIcon,
13
+ removeIcon,
14
+ shieldIcon,
15
+ starFilledIcon
16
+ } from './icons';
10
17
  import {
11
18
  IFavouriteResponse,
12
19
  ILaunchTerminalResponse,
@@ -125,6 +132,11 @@ export class ClaudeCodeSessionsWidget extends Widget {
125
132
  this._render();
126
133
  }
127
134
 
135
+ /** Toggle the --dangerously-skip-permissions flag on launched sessions. */
136
+ setDangerouslySkipPermissions(on: boolean): void {
137
+ this._dangerouslySkip = !!on;
138
+ }
139
+
128
140
  protected onAfterShow(_msg: Message): void {
129
141
  this.refresh();
130
142
  this._startPolling();
@@ -199,11 +211,11 @@ export class ClaudeCodeSessionsWidget extends Widget {
199
211
  .replace(/[\s\-_./]+/g, '');
200
212
  }
201
213
 
202
- /** Strict fuzzy match: substring on normalised strings, with up to 2%
203
- * Levenshtein tolerance for queries of 6+ chars (so the 98% threshold
204
- * only relaxes substring strictness when the query is long enough that
205
- * 2% rounds to a nonzero edit budget - effectively substring-only for
206
- * short queries).
214
+ /** Fuzzy match at a 95% threshold: substring on normalised strings,
215
+ * with up to 5% Levenshtein tolerance. For short queries the budget
216
+ * still rounds to zero so behaviour is substring-only there - the
217
+ * relaxation only kicks in for queries long enough that 5% reaches a
218
+ * full edit (10+ chars).
207
219
  */
208
220
  private _fuzzyMatch(haystack: string, needle: string): boolean {
209
221
  if (!needle) {
@@ -217,7 +229,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
217
229
  if (h.includes(n)) {
218
230
  return true;
219
231
  }
220
- const tol = Math.floor(n.length * 0.02);
232
+ const tol = Math.round(n.length * 0.05);
221
233
  if (tol === 0) {
222
234
  return false;
223
235
  }
@@ -344,38 +356,58 @@ export class ClaudeCodeSessionsWidget extends Widget {
344
356
 
345
357
  // -------------------------------------------------------------- terminal
346
358
 
347
- private async _resumeInTerminal(session: ISession): Promise<void> {
359
+ private async _resumeInTerminal(
360
+ session: ISession,
361
+ forceDangerous: boolean = false
362
+ ): Promise<void> {
348
363
  // Coalesce concurrent clicks on the same row - subsequent clicks attach
349
364
  // to the in-flight promise instead of creating their own terminal.
350
365
  const inFlight = this._pendingByPath.get(session.project_path);
351
366
  if (inFlight) {
352
367
  return inFlight;
353
368
  }
354
- const promise = this._doResumeInTerminal(session).finally(() => {
355
- this._pendingByPath.delete(session.project_path);
356
- });
369
+ const promise = this._doResumeInTerminal(session, forceDangerous).finally(
370
+ () => {
371
+ this._pendingByPath.delete(session.project_path);
372
+ }
373
+ );
357
374
  this._pendingByPath.set(session.project_path, promise);
358
375
  return promise;
359
376
  }
360
377
 
361
- private async _doResumeInTerminal(session: ISession): Promise<void> {
378
+ private async _doResumeInTerminal(
379
+ session: ISession,
380
+ forceDangerous: boolean
381
+ ): Promise<void> {
362
382
  try {
363
- // 1. In-memory microcache - covers rapid-click and post-creation reuse
364
- // before the new widget propagates fully through the tracker. Cleared
365
- // on widget disposal. NOT persisted to localStorage.
383
+ // Always prefer reusing an open terminal for this project. The
384
+ // skip-permissions flag can only be applied to a fresh pty, never
385
+ // retroactively. So if the user wants dangerous mode but an open
386
+ // terminal already exists, show a modal asking them to close it
387
+ // first - we won't auto-close, won't silently reuse the wrong mode.
388
+
389
+ // 1. In-memory microcache.
366
390
  const cached = this._terminalsByPath.get(session.project_path);
367
391
  if (cached && !cached.isDisposed) {
392
+ if (forceDangerous) {
393
+ await this._showCloseExistingDialog();
394
+ this._app.shell.activateById(cached.id);
395
+ return;
396
+ }
368
397
  this._app.shell.activateById(cached.id);
369
398
  return;
370
399
  }
371
400
 
372
- // 2. Walk every live terminal widget JL knows about and ask the
373
- // server for the cwd of EVERY process in its pty's tree. Match if
374
- // any one of them equals project_path.
401
+ // 2. Walk every live terminal widget JL knows about.
375
402
  const found = await this._findTerminalForCwd(session.project_path);
376
403
  if (found) {
377
404
  this._terminalsByPath.set(session.project_path, found);
378
405
  this._wireTerminalDisposal(session.project_path, found);
406
+ if (forceDangerous) {
407
+ await this._showCloseExistingDialog();
408
+ this._app.shell.activateById(found.id);
409
+ return;
410
+ }
379
411
  this._app.shell.activateById(found.id);
380
412
  return;
381
413
  }
@@ -392,7 +424,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
392
424
  method: 'POST',
393
425
  body: JSON.stringify({
394
426
  project_path: session.project_path,
395
- session_id: session.session_id
427
+ session_id: session.session_id,
428
+ dangerously_skip_permissions:
429
+ forceDangerous || this._dangerouslySkip
396
430
  })
397
431
  }
398
432
  );
@@ -443,6 +477,17 @@ export class ClaudeCodeSessionsWidget extends Widget {
443
477
  return null;
444
478
  }
445
479
 
480
+ private async _showCloseExistingDialog(): Promise<void> {
481
+ await showDialog({
482
+ title: 'Existing Claude session is running',
483
+ body:
484
+ 'A terminal for this project is already open. To launch with ' +
485
+ '--dangerously-skip-permissions, close that terminal first then ' +
486
+ 'click "Resume (Skip Permissions)" again.',
487
+ buttons: [Dialog.okButton({ label: 'OK' })]
488
+ });
489
+ }
490
+
446
491
  private _wireTerminalDisposal(projectPath: string, widget: any): void {
447
492
  if (!widget?.disposed?.connect) {
448
493
  return;
@@ -779,6 +824,16 @@ export class ClaudeCodeSessionsWidget extends Widget {
779
824
  }
780
825
  });
781
826
 
827
+ this._commands.addCommand('claude-code-sessions:resume-dangerous', {
828
+ label: 'Resume (Skip Permissions)',
829
+ icon: shieldIcon,
830
+ execute: () => {
831
+ if (this._activeSession) {
832
+ void this._resumeInTerminal(this._activeSession, true);
833
+ }
834
+ }
835
+ });
836
+
782
837
  this._commands.addCommand('claude-code-sessions:remove', {
783
838
  label: 'Remove from Claude',
784
839
  icon: removeIcon,
@@ -792,6 +847,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
792
847
  this._contextMenu = new Menu({ commands: this._commands });
793
848
  this._contextMenu.addClass('jp-ClaudeSessionsContextMenu');
794
849
  this._contextMenu.addItem({ command: 'claude-code-sessions:resume' });
850
+ this._contextMenu.addItem({
851
+ command: 'claude-code-sessions:resume-dangerous'
852
+ });
795
853
  this._contextMenu.addItem({
796
854
  command: 'claude-code-sessions:toggle-favourite'
797
855
  });
@@ -848,6 +906,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
848
906
  private readonly _rootDir: string;
849
907
  private _resolveNames: boolean = true;
850
908
  private _recentLimit: number = DEFAULT_RECENT_LIMIT;
909
+ private _dangerouslySkip: boolean = false;
851
910
  private _displayNames: Map<string, string> = new Map();
852
911
  private _filter: string = '';
853
912
  }