jupyterlab_claude_code_extension 1.1.25 → 1.1.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 +1 -0
- package/lib/icons.js +9 -0
- package/lib/index.js +9 -3
- package/lib/types.d.ts +2 -1
- package/lib/widget.d.ts +13 -1
- package/lib/widget.js +79 -2
- package/package.json +2 -1
- package/src/icons.ts +11 -0
- package/src/index.ts +11 -3
- package/src/types.ts +2 -1
- package/src/widget.ts +91 -1
package/lib/icons.d.ts
CHANGED
package/lib/icons.js
CHANGED
|
@@ -49,6 +49,15 @@ 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
|
+
});
|
|
52
61
|
// Funnel copied verbatim from @jupyterlab/ui-components'
|
|
53
62
|
// `search/filter.svg` - the same image the file browser's filter
|
|
54
63
|
// 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: [
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
@@ -97,11 +107,13 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
97
107
|
private _expanded;
|
|
98
108
|
private _commands;
|
|
99
109
|
private _contextMenu;
|
|
110
|
+
private _newSessionMenu;
|
|
100
111
|
private _activeSession;
|
|
101
112
|
private _activeRowEl;
|
|
102
113
|
private _pollHandle;
|
|
103
114
|
private readonly _removingPaths;
|
|
104
115
|
private readonly _terminalTracker;
|
|
116
|
+
private readonly _fileBrowser;
|
|
105
117
|
private readonly _terminalsByPath;
|
|
106
118
|
private readonly _pendingByPath;
|
|
107
119
|
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, 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,16 @@ 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', () => {
|
|
148
|
+
// Drop the menu just below the button, left-aligned with it.
|
|
149
|
+
const rect = newBtn.getBoundingClientRect();
|
|
150
|
+
this._newSessionMenu.open(rect.left, rect.bottom);
|
|
151
|
+
});
|
|
152
|
+
header.appendChild(newBtn);
|
|
142
153
|
const filterBtn = document.createElement('button');
|
|
143
154
|
filterBtn.className = 'jp-ClaudeSessionsPanel-iconButton';
|
|
144
155
|
filterBtn.title = 'Filter sessions';
|
|
@@ -464,6 +475,53 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
464
475
|
});
|
|
465
476
|
}
|
|
466
477
|
}
|
|
478
|
+
/** Absolute path of the file browser's current folder; falls back to the
|
|
479
|
+
* server root when no file browser is available. */
|
|
480
|
+
_currentFolder() {
|
|
481
|
+
var _a, _b, _c;
|
|
482
|
+
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(/^\/+/, '');
|
|
483
|
+
return rel ? `${this._rootDir}/${rel}` : this._rootDir;
|
|
484
|
+
}
|
|
485
|
+
/** Start a brand-new claude session in the file browser's current folder.
|
|
486
|
+
* Same launch path as resuming (claude is the pty's only process via the
|
|
487
|
+
* launch-terminal endpoint) - just without --resume, and always a fresh
|
|
488
|
+
* terminal since there is no existing session to reuse.
|
|
489
|
+
*/
|
|
490
|
+
async _newSession(forceDangerous) {
|
|
491
|
+
const projectPath = this._currentFolder();
|
|
492
|
+
if (!projectPath) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const spinner = this._showLaunchSpinner();
|
|
496
|
+
try {
|
|
497
|
+
const launched = await requestAPI('launch-terminal', this._serverSettings, {
|
|
498
|
+
method: 'POST',
|
|
499
|
+
body: JSON.stringify({
|
|
500
|
+
project_path: projectPath,
|
|
501
|
+
dangerously_skip_permissions: forceDangerous || this._dangerouslySkip
|
|
502
|
+
})
|
|
503
|
+
});
|
|
504
|
+
const widget = await this._app.commands.execute('terminal:open', {
|
|
505
|
+
name: launched.terminal_name
|
|
506
|
+
});
|
|
507
|
+
if (widget === null || widget === void 0 ? void 0 : widget.id) {
|
|
508
|
+
this._terminalsByPath.set(projectPath, widget);
|
|
509
|
+
this._wireTerminalDisposal(projectPath, widget);
|
|
510
|
+
this._focusTerminal(widget);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
this._showError(err);
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
spinner.dispose();
|
|
518
|
+
// The new session creates a project dir under ~/.claude - refresh so
|
|
519
|
+
// its row (and remote-control state) appears without waiting a poll.
|
|
520
|
+
void this._fetch().catch(() => {
|
|
521
|
+
/* a poll tick will retry; nothing actionable here */
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
467
525
|
/**
|
|
468
526
|
* Bring a terminal tab to the front AND hand it keyboard focus, so the
|
|
469
527
|
* user can start typing without an extra click. `activateById` only
|
|
@@ -963,6 +1021,25 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
963
1021
|
}
|
|
964
1022
|
}
|
|
965
1023
|
});
|
|
1024
|
+
this._commands.addCommand('claude-code-sessions:new-session', {
|
|
1025
|
+
label: 'New Claude Session',
|
|
1026
|
+
execute: () => void this._newSession(false)
|
|
1027
|
+
});
|
|
1028
|
+
this._commands.addCommand('claude-code-sessions:new-session-dangerous', {
|
|
1029
|
+
label: 'New Claude Session (Skip Permissions)',
|
|
1030
|
+
icon: shieldIcon,
|
|
1031
|
+
execute: () => void this._newSession(true)
|
|
1032
|
+
});
|
|
1033
|
+
// Dropdown for the header's plus button - same command registry and
|
|
1034
|
+
// styling as the row context menu.
|
|
1035
|
+
this._newSessionMenu = new Menu({ commands: this._commands });
|
|
1036
|
+
this._newSessionMenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
1037
|
+
this._newSessionMenu.addItem({
|
|
1038
|
+
command: 'claude-code-sessions:new-session'
|
|
1039
|
+
});
|
|
1040
|
+
this._newSessionMenu.addItem({
|
|
1041
|
+
command: 'claude-code-sessions:new-session-dangerous'
|
|
1042
|
+
});
|
|
966
1043
|
this._contextMenu = new Menu({ commands: this._commands });
|
|
967
1044
|
this._contextMenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
968
1045
|
this._contextMenu.addItem({ command: 'claude-code-sessions:resume' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_claude_code_extension",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -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,17 @@ 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
|
+
|
|
63
74
|
// Funnel copied verbatim from @jupyterlab/ui-components'
|
|
64
75
|
// `search/filter.svg` - the same image the file browser's filter
|
|
65
76
|
// 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: [
|
|
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
|
-
|
|
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,7 @@ import { Message } from '@lumino/messaging';
|
|
|
14
15
|
|
|
15
16
|
import { requestAPI } from './request';
|
|
16
17
|
import {
|
|
18
|
+
addIcon,
|
|
17
19
|
claudeIcon,
|
|
18
20
|
filterIcon,
|
|
19
21
|
refreshIcon,
|
|
@@ -101,13 +103,15 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
101
103
|
constructor(
|
|
102
104
|
app: JupyterFrontEnd,
|
|
103
105
|
rootDir: string,
|
|
104
|
-
terminalTracker: ITerminalTracker | null = null
|
|
106
|
+
terminalTracker: ITerminalTracker | null = null,
|
|
107
|
+
fileBrowser: IDefaultFileBrowser | null = null
|
|
105
108
|
) {
|
|
106
109
|
super();
|
|
107
110
|
this._app = app;
|
|
108
111
|
this._serverSettings = app.serviceManager.serverSettings;
|
|
109
112
|
this._rootDir = rootDir.replace(/\/+$/, '');
|
|
110
113
|
this._terminalTracker = terminalTracker;
|
|
114
|
+
this._fileBrowser = fileBrowser;
|
|
111
115
|
|
|
112
116
|
this.id = 'jupyterlab-claude-code-extension';
|
|
113
117
|
this.title.icon = claudeIcon;
|
|
@@ -185,6 +189,17 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
185
189
|
title.textContent = 'Claude Code Sessions';
|
|
186
190
|
header.appendChild(title);
|
|
187
191
|
|
|
192
|
+
const newBtn = document.createElement('button');
|
|
193
|
+
newBtn.className = 'jp-ClaudeSessionsPanel-iconButton';
|
|
194
|
+
newBtn.title = 'New Claude session in the current folder';
|
|
195
|
+
addIcon.element({ container: newBtn });
|
|
196
|
+
newBtn.addEventListener('click', () => {
|
|
197
|
+
// Drop the menu just below the button, left-aligned with it.
|
|
198
|
+
const rect = newBtn.getBoundingClientRect();
|
|
199
|
+
this._newSessionMenu.open(rect.left, rect.bottom);
|
|
200
|
+
});
|
|
201
|
+
header.appendChild(newBtn);
|
|
202
|
+
|
|
188
203
|
const filterBtn = document.createElement('button');
|
|
189
204
|
filterBtn.className = 'jp-ClaudeSessionsPanel-iconButton';
|
|
190
205
|
filterBtn.title = 'Filter sessions';
|
|
@@ -568,6 +583,57 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
568
583
|
}
|
|
569
584
|
}
|
|
570
585
|
|
|
586
|
+
/** Absolute path of the file browser's current folder; falls back to the
|
|
587
|
+
* server root when no file browser is available. */
|
|
588
|
+
private _currentFolder(): string {
|
|
589
|
+
const rel = (this._fileBrowser?.model?.path ?? '').replace(/^\/+/, '');
|
|
590
|
+
return rel ? `${this._rootDir}/${rel}` : this._rootDir;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Start a brand-new claude session in the file browser's current folder.
|
|
594
|
+
* Same launch path as resuming (claude is the pty's only process via the
|
|
595
|
+
* launch-terminal endpoint) - just without --resume, and always a fresh
|
|
596
|
+
* terminal since there is no existing session to reuse.
|
|
597
|
+
*/
|
|
598
|
+
private async _newSession(forceDangerous: boolean): Promise<void> {
|
|
599
|
+
const projectPath = this._currentFolder();
|
|
600
|
+
if (!projectPath) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const spinner = this._showLaunchSpinner();
|
|
604
|
+
try {
|
|
605
|
+
const launched = await requestAPI<ILaunchTerminalResponse>(
|
|
606
|
+
'launch-terminal',
|
|
607
|
+
this._serverSettings,
|
|
608
|
+
{
|
|
609
|
+
method: 'POST',
|
|
610
|
+
body: JSON.stringify({
|
|
611
|
+
project_path: projectPath,
|
|
612
|
+
dangerously_skip_permissions:
|
|
613
|
+
forceDangerous || this._dangerouslySkip
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
);
|
|
617
|
+
const widget: any = await this._app.commands.execute('terminal:open', {
|
|
618
|
+
name: launched.terminal_name
|
|
619
|
+
});
|
|
620
|
+
if (widget?.id) {
|
|
621
|
+
this._terminalsByPath.set(projectPath, widget);
|
|
622
|
+
this._wireTerminalDisposal(projectPath, widget);
|
|
623
|
+
this._focusTerminal(widget);
|
|
624
|
+
}
|
|
625
|
+
} catch (err) {
|
|
626
|
+
this._showError(err);
|
|
627
|
+
} finally {
|
|
628
|
+
spinner.dispose();
|
|
629
|
+
// The new session creates a project dir under ~/.claude - refresh so
|
|
630
|
+
// its row (and remote-control state) appears without waiting a poll.
|
|
631
|
+
void this._fetch().catch(() => {
|
|
632
|
+
/* a poll tick will retry; nothing actionable here */
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
571
637
|
/**
|
|
572
638
|
* Bring a terminal tab to the front AND hand it keyboard focus, so the
|
|
573
639
|
* user can start typing without an extra click. `activateById` only
|
|
@@ -1124,6 +1190,28 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1124
1190
|
}
|
|
1125
1191
|
});
|
|
1126
1192
|
|
|
1193
|
+
this._commands.addCommand('claude-code-sessions:new-session', {
|
|
1194
|
+
label: 'New Claude Session',
|
|
1195
|
+
execute: () => void this._newSession(false)
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
this._commands.addCommand('claude-code-sessions:new-session-dangerous', {
|
|
1199
|
+
label: 'New Claude Session (Skip Permissions)',
|
|
1200
|
+
icon: shieldIcon,
|
|
1201
|
+
execute: () => void this._newSession(true)
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
// Dropdown for the header's plus button - same command registry and
|
|
1205
|
+
// styling as the row context menu.
|
|
1206
|
+
this._newSessionMenu = new Menu({ commands: this._commands });
|
|
1207
|
+
this._newSessionMenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
1208
|
+
this._newSessionMenu.addItem({
|
|
1209
|
+
command: 'claude-code-sessions:new-session'
|
|
1210
|
+
});
|
|
1211
|
+
this._newSessionMenu.addItem({
|
|
1212
|
+
command: 'claude-code-sessions:new-session-dangerous'
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1127
1215
|
this._contextMenu = new Menu({ commands: this._commands });
|
|
1128
1216
|
this._contextMenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
1129
1217
|
this._contextMenu.addItem({ command: 'claude-code-sessions:resume' });
|
|
@@ -1187,11 +1275,13 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1187
1275
|
private _expanded: Record<SectionKey, boolean> = loadExpanded();
|
|
1188
1276
|
private _commands!: CommandRegistry;
|
|
1189
1277
|
private _contextMenu!: Menu;
|
|
1278
|
+
private _newSessionMenu!: Menu;
|
|
1190
1279
|
private _activeSession: ISession | null = null;
|
|
1191
1280
|
private _activeRowEl: HTMLElement | null = null;
|
|
1192
1281
|
private _pollHandle: number | null = null;
|
|
1193
1282
|
private readonly _removingPaths: Set<string> = new Set();
|
|
1194
1283
|
private readonly _terminalTracker: ITerminalTracker | null;
|
|
1284
|
+
private readonly _fileBrowser: IDefaultFileBrowser | null;
|
|
1195
1285
|
private readonly _terminalsByPath: Map<string, any> = new Map();
|
|
1196
1286
|
private readonly _pendingByPath: Map<string, Promise<void>> = new Map();
|
|
1197
1287
|
private readonly _rootDir: string;
|