jupyterlab_claude_code_extension 1.0.47 → 1.0.53
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 +9 -10
- package/lib/index.js +20 -1
- package/lib/types.d.ts +1 -0
- package/lib/widget.d.ts +12 -1
- package/lib/widget.js +78 -21
- package/package.json +1 -1
- package/src/index.ts +23 -1
- package/src/types.ts +1 -0
- package/src/widget.ts +92 -25
- package/style/base.css +23 -0
package/README.md
CHANGED
|
@@ -8,21 +8,20 @@
|
|
|
8
8
|
[](https://kolomolo.com)
|
|
9
9
|
[](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)
|
|
10
10
|
|
|
11
|
-
|
|
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
|

|
|
14
14
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
|
-
- **Three-section side panel** - Favourites, Recent
|
|
18
|
-
- **Live
|
|
19
|
-
- **One-click resume** - click a row to
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **Auto-disabled** when the
|
|
25
|
-
- **Hover tooltip** with relative path (vs JL root), last activity, message count, branch, first prompt, session id
|
|
17
|
+
- **Three-section side panel** - Favourites, 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
|
+
- **Favourites** - 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
|
@@ -26,10 +26,17 @@ const plugin = {
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
const widget = new ClaudeCodeSessionsWidget(app, status.root_dir || '', terminalTracker);
|
|
29
|
-
|
|
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
package/lib/widget.d.ts
CHANGED
|
@@ -40,12 +40,23 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
40
40
|
private _doResumeInTerminal;
|
|
41
41
|
private _findTerminalForCwd;
|
|
42
42
|
private _showCloseExistingDialog;
|
|
43
|
+
/** Show a modal with a spinner while the terminal is being launched. The
|
|
44
|
+
* caller must dismiss it via ``.resolve()`` once the work is done.
|
|
45
|
+
*/
|
|
46
|
+
private _showLaunchSpinner;
|
|
43
47
|
private _wireTerminalDisposal;
|
|
44
48
|
/** Apply the presentation-mode setting (path-segment disambiguation is
|
|
45
49
|
* handled separately in ``_disambiguate``). */
|
|
46
50
|
private _displayName;
|
|
47
51
|
private _basename;
|
|
48
|
-
/** 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
|
+
*/
|
|
49
60
|
private _disambiguate;
|
|
50
61
|
private _render;
|
|
51
62
|
private _renderSection;
|
package/lib/widget.js
CHANGED
|
@@ -338,21 +338,29 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
338
338
|
// as the pty's only process (no shell). Server-side endpoint calls
|
|
339
339
|
// terminal_manager.create(shell_command=[claude, --resume, sid], cwd=...)
|
|
340
340
|
// and returns the terminal name; we then attach JL's standard widget
|
|
341
|
-
// via terminal:open. When claude exits, the tab closes.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
this.
|
|
355
|
-
|
|
341
|
+
// via terminal:open. When claude exits, the tab closes. The launch
|
|
342
|
+
// RPC + the WebSocket-resize waiter on the server can take a few
|
|
343
|
+
// seconds, so show a modal spinner for visual feedback.
|
|
344
|
+
const spinner = this._showLaunchSpinner(`Opening ${this._lookupName(session)}...`);
|
|
345
|
+
try {
|
|
346
|
+
const launched = await requestAPI('launch-terminal', this._serverSettings, {
|
|
347
|
+
method: 'POST',
|
|
348
|
+
body: JSON.stringify({
|
|
349
|
+
project_path: session.project_path,
|
|
350
|
+
session_id: session.session_id,
|
|
351
|
+
dangerously_skip_permissions: forceDangerous || this._dangerouslySkip
|
|
352
|
+
})
|
|
353
|
+
});
|
|
354
|
+
const widget = await this._app.commands.execute('terminal:open', {
|
|
355
|
+
name: launched.terminal_name
|
|
356
|
+
});
|
|
357
|
+
if (widget === null || widget === void 0 ? void 0 : widget.id) {
|
|
358
|
+
this._terminalsByPath.set(session.project_path, widget);
|
|
359
|
+
this._wireTerminalDisposal(session.project_path, widget);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
spinner.resolve();
|
|
356
364
|
}
|
|
357
365
|
}
|
|
358
366
|
catch (err) {
|
|
@@ -407,6 +415,30 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
407
415
|
buttons: [Dialog.okButton({ label: 'OK' })]
|
|
408
416
|
});
|
|
409
417
|
}
|
|
418
|
+
/** Show a modal with a spinner while the terminal is being launched. The
|
|
419
|
+
* caller must dismiss it via ``.resolve()`` once the work is done.
|
|
420
|
+
*/
|
|
421
|
+
_showLaunchSpinner(label) {
|
|
422
|
+
const body = new Widget();
|
|
423
|
+
body.node.className = 'jp-ClaudeSessionsPanel-launchOverlay';
|
|
424
|
+
const spinner = document.createElement('div');
|
|
425
|
+
spinner.className =
|
|
426
|
+
'jp-claude-sessions-panel-spinner jp-ClaudeSessionsPanel-launchSpinner';
|
|
427
|
+
body.node.appendChild(spinner);
|
|
428
|
+
const text = document.createElement('div');
|
|
429
|
+
text.className = 'jp-ClaudeSessionsPanel-launchLabel';
|
|
430
|
+
text.textContent = label;
|
|
431
|
+
body.node.appendChild(text);
|
|
432
|
+
const dialog = new Dialog({
|
|
433
|
+
title: 'Opening Claude Code session',
|
|
434
|
+
body,
|
|
435
|
+
buttons: [Dialog.cancelButton({ label: 'Run in background' })]
|
|
436
|
+
});
|
|
437
|
+
// launch() returns a Promise we don't await - we resolve programmatically
|
|
438
|
+
// when the spawn completes (or errors).
|
|
439
|
+
void dialog.launch();
|
|
440
|
+
return dialog;
|
|
441
|
+
}
|
|
410
442
|
_wireTerminalDisposal(projectPath, widget) {
|
|
411
443
|
var _a;
|
|
412
444
|
if (!((_a = widget === null || widget === void 0 ? void 0 : widget.disposed) === null || _a === void 0 ? void 0 : _a.connect)) {
|
|
@@ -440,7 +472,14 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
440
472
|
const parts = p.split('/').filter(Boolean);
|
|
441
473
|
return parts[parts.length - 1] || '';
|
|
442
474
|
}
|
|
443
|
-
/** 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
|
+
*/
|
|
444
483
|
_disambiguate(rows) {
|
|
445
484
|
var _a;
|
|
446
485
|
const out = new Map();
|
|
@@ -454,20 +493,38 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
454
493
|
out.set(group[0].project_path, name);
|
|
455
494
|
continue;
|
|
456
495
|
}
|
|
457
|
-
const
|
|
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));
|
|
458
511
|
const max = Math.max(...segs.map(s => s.length));
|
|
459
512
|
let depth = 1;
|
|
513
|
+
let resolved = false;
|
|
460
514
|
while (depth <= max) {
|
|
461
515
|
const tails = segs.map(s => s.slice(-depth).join('/'));
|
|
462
|
-
|
|
463
|
-
|
|
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;
|
|
464
521
|
break;
|
|
465
522
|
}
|
|
466
523
|
depth += 1;
|
|
467
524
|
}
|
|
468
|
-
if (!
|
|
525
|
+
if (!resolved) {
|
|
469
526
|
// Fallback to absolute path
|
|
470
|
-
|
|
527
|
+
basenames.forEach(r => out.set(r.project_path, r.project_path));
|
|
471
528
|
}
|
|
472
529
|
}
|
|
473
530
|
return out;
|
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.53",
|
|
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/index.ts
CHANGED
|
@@ -53,11 +53,20 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
53
53
|
status.root_dir || '',
|
|
54
54
|
terminalTracker
|
|
55
55
|
);
|
|
56
|
-
|
|
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
package/src/widget.ts
CHANGED
|
@@ -415,26 +415,35 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
415
415
|
// as the pty's only process (no shell). Server-side endpoint calls
|
|
416
416
|
// terminal_manager.create(shell_command=[claude, --resume, sid], cwd=...)
|
|
417
417
|
// and returns the terminal name; we then attach JL's standard widget
|
|
418
|
-
// via terminal:open. When claude exits, the tab closes.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
{
|
|
423
|
-
method: 'POST',
|
|
424
|
-
body: JSON.stringify({
|
|
425
|
-
project_path: session.project_path,
|
|
426
|
-
session_id: session.session_id,
|
|
427
|
-
dangerously_skip_permissions:
|
|
428
|
-
forceDangerous || this._dangerouslySkip
|
|
429
|
-
})
|
|
430
|
-
}
|
|
418
|
+
// via terminal:open. When claude exits, the tab closes. The launch
|
|
419
|
+
// RPC + the WebSocket-resize waiter on the server can take a few
|
|
420
|
+
// seconds, so show a modal spinner for visual feedback.
|
|
421
|
+
const spinner = this._showLaunchSpinner(
|
|
422
|
+
`Opening ${this._lookupName(session)}...`
|
|
431
423
|
);
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
424
|
+
try {
|
|
425
|
+
const launched = await requestAPI<ILaunchTerminalResponse>(
|
|
426
|
+
'launch-terminal',
|
|
427
|
+
this._serverSettings,
|
|
428
|
+
{
|
|
429
|
+
method: 'POST',
|
|
430
|
+
body: JSON.stringify({
|
|
431
|
+
project_path: session.project_path,
|
|
432
|
+
session_id: session.session_id,
|
|
433
|
+
dangerously_skip_permissions:
|
|
434
|
+
forceDangerous || this._dangerouslySkip
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
const widget: any = await this._app.commands.execute('terminal:open', {
|
|
439
|
+
name: launched.terminal_name
|
|
440
|
+
});
|
|
441
|
+
if (widget?.id) {
|
|
442
|
+
this._terminalsByPath.set(session.project_path, widget);
|
|
443
|
+
this._wireTerminalDisposal(session.project_path, widget);
|
|
444
|
+
}
|
|
445
|
+
} finally {
|
|
446
|
+
spinner.resolve();
|
|
438
447
|
}
|
|
439
448
|
} catch (err) {
|
|
440
449
|
this._showError(err);
|
|
@@ -493,6 +502,34 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
493
502
|
});
|
|
494
503
|
}
|
|
495
504
|
|
|
505
|
+
/** Show a modal with a spinner while the terminal is being launched. The
|
|
506
|
+
* caller must dismiss it via ``.resolve()`` once the work is done.
|
|
507
|
+
*/
|
|
508
|
+
private _showLaunchSpinner(label: string): Dialog<unknown> {
|
|
509
|
+
const body = new Widget();
|
|
510
|
+
body.node.className = 'jp-ClaudeSessionsPanel-launchOverlay';
|
|
511
|
+
|
|
512
|
+
const spinner = document.createElement('div');
|
|
513
|
+
spinner.className =
|
|
514
|
+
'jp-claude-sessions-panel-spinner jp-ClaudeSessionsPanel-launchSpinner';
|
|
515
|
+
body.node.appendChild(spinner);
|
|
516
|
+
|
|
517
|
+
const text = document.createElement('div');
|
|
518
|
+
text.className = 'jp-ClaudeSessionsPanel-launchLabel';
|
|
519
|
+
text.textContent = label;
|
|
520
|
+
body.node.appendChild(text);
|
|
521
|
+
|
|
522
|
+
const dialog = new Dialog<unknown>({
|
|
523
|
+
title: 'Opening Claude Code session',
|
|
524
|
+
body,
|
|
525
|
+
buttons: [Dialog.cancelButton({ label: 'Run in background' })]
|
|
526
|
+
});
|
|
527
|
+
// launch() returns a Promise we don't await - we resolve programmatically
|
|
528
|
+
// when the spawn completes (or errors).
|
|
529
|
+
void dialog.launch();
|
|
530
|
+
return dialog;
|
|
531
|
+
}
|
|
532
|
+
|
|
496
533
|
private _wireTerminalDisposal(projectPath: string, widget: any): void {
|
|
497
534
|
if (!widget?.disposed?.connect) {
|
|
498
535
|
return;
|
|
@@ -529,7 +566,14 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
529
566
|
return parts[parts.length - 1] || '';
|
|
530
567
|
}
|
|
531
568
|
|
|
532
|
-
/** 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
|
+
*/
|
|
533
577
|
private _disambiguate(rows: ISession[]): Map<string, string> {
|
|
534
578
|
const out = new Map<string, string>();
|
|
535
579
|
const groups = new Map<string, ISession[]>();
|
|
@@ -542,20 +586,43 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
542
586
|
out.set(group[0].project_path, name);
|
|
543
587
|
continue;
|
|
544
588
|
}
|
|
545
|
-
|
|
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
|
+
);
|
|
546
609
|
const max = Math.max(...segs.map(s => s.length));
|
|
547
610
|
let depth = 1;
|
|
611
|
+
let resolved = false;
|
|
548
612
|
while (depth <= max) {
|
|
549
613
|
const tails = segs.map(s => s.slice(-depth).join('/'));
|
|
550
|
-
|
|
551
|
-
|
|
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;
|
|
552
619
|
break;
|
|
553
620
|
}
|
|
554
621
|
depth += 1;
|
|
555
622
|
}
|
|
556
|
-
if (!
|
|
623
|
+
if (!resolved) {
|
|
557
624
|
// Fallback to absolute path
|
|
558
|
-
|
|
625
|
+
basenames.forEach(r => out.set(r.project_path, r.project_path));
|
|
559
626
|
}
|
|
560
627
|
}
|
|
561
628
|
return out;
|
package/style/base.css
CHANGED
|
@@ -237,6 +237,29 @@
|
|
|
237
237
|
display: inline-block;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
.jp-ClaudeSessionsPanel-launchOverlay {
|
|
241
|
+
display: flex;
|
|
242
|
+
flex-direction: column;
|
|
243
|
+
align-items: center;
|
|
244
|
+
justify-content: center;
|
|
245
|
+
gap: 12px;
|
|
246
|
+
padding: 12px 8px;
|
|
247
|
+
min-width: 240px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.jp-ClaudeSessionsPanel-launchSpinner {
|
|
251
|
+
width: 28px;
|
|
252
|
+
height: 28px;
|
|
253
|
+
border-width: 3px;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.jp-ClaudeSessionsPanel-launchLabel {
|
|
257
|
+
font-size: var(--jp-ui-font-size1);
|
|
258
|
+
color: var(--jp-ui-font-color1);
|
|
259
|
+
text-align: center;
|
|
260
|
+
word-break: break-word;
|
|
261
|
+
}
|
|
262
|
+
|
|
240
263
|
.jp-ClaudeSessionsPanel-row.jp-mod-busy {
|
|
241
264
|
opacity: 0.55;
|
|
242
265
|
pointer-events: none;
|