jupyterlab_claude_code_extension 1.0.16
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/LICENSE +29 -0
- package/README.md +51 -0
- package/lib/icons.d.ts +5 -0
- package/lib/icons.js +44 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +57 -0
- package/lib/request.d.ts +10 -0
- package/lib/request.js +35 -0
- package/lib/types.d.ts +36 -0
- package/lib/types.js +1 -0
- package/lib/widget.d.ts +60 -0
- package/lib/widget.js +643 -0
- package/package.json +217 -0
- package/src/__tests__/jupyterlab_claude_code_extension.spec.ts +62 -0
- package/src/icons.ts +52 -0
- package/src/index.ts +95 -0
- package/src/request.ts +51 -0
- package/src/types.ts +42 -0
- package/src/widget.ts +764 -0
- package/style/base.css +246 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Stellars Henson
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# jupyterlab_claude_code_extension
|
|
2
|
+
|
|
3
|
+
[](https://github.com/stellarshenson/jupyterlab_claude_code_extension/actions/workflows/build.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/jupyterlab_claude_code_extension)
|
|
5
|
+
[](https://pypi.org/project/jupyterlab_claude_code_extension/)
|
|
6
|
+
[](https://pepy.tech/project/jupyterlab_claude_code_extension)
|
|
7
|
+
[](https://jupyterlab.readthedocs.io/en/stable/)
|
|
8
|
+
[](https://kolomolo.com)
|
|
9
|
+
[](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)
|
|
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>`.
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## Features
|
|
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
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- JupyterLab >= 4.0.0
|
|
30
|
+
- Python >= 3.10
|
|
31
|
+
- `claude` CLI on `PATH`
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
Developers must install via the project `Makefile` (which orchestrates clean, build, and pip install of the resulting wheel):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
make install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
End-users can install the published package from PyPI:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install jupyterlab_claude_code_extension
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Uninstall
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip uninstall jupyterlab_claude_code_extension
|
|
51
|
+
```
|
package/lib/icons.d.ts
ADDED
package/lib/icons.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
const claudeSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
|
3
|
+
<g class="jp-icon3" fill="#616161">
|
|
4
|
+
<path d="m14.375 6.48.49.28v.209l-.14.489-5.937 1.397-.558-1.387zm0 0"/>
|
|
5
|
+
<path d="m12.155 2.373.683.143.182.224.173.535-.072.342-3.983 5.447L7.81 7.737l3.673-4.82z"/>
|
|
6
|
+
<path d="m8.719 1.522.419-.28.349.14.349.49-.957 5.748-.65-.441-.279-.769.49-4.33z"/>
|
|
7
|
+
<path d="m4.239 1.614.43-.55L4.95 1l.558.081.275.216 2.004 4.442.724 2.11-.848.471-3.231-5.864z"/>
|
|
8
|
+
<path d="m2.154 4.665-.14-.56.42-.488.488.07h.14l2.933 2.165.908.698 1.257.978-.698 1.187-.629-.489-.419-.419-4.05-2.863z"/>
|
|
9
|
+
<path d="M1.316 8.296 1 7.946v-.31l.316-.108 3.562.21 3.491.279-.113.695-6.66-.346z"/>
|
|
10
|
+
<path d="M3.411 11.931h-.698l-.278-.32v-.382l1.186-.838 4.82-3.068.487.833z"/>
|
|
11
|
+
<path d="m4.738 13.883-.28.07-.418-.21.07-.35 4.12-5.446.558.768-3.072 4.05z"/>
|
|
12
|
+
<path d="m8.23 14.581-.21.28-.419.14-.349-.28-.21-.42L8.09 8.646l.629.07z"/>
|
|
13
|
+
<path d="M11.791 13.045v.558l-.07.21-.279.14-.489-.066-3.356-4.996 1.331-1.014 1.117 2.025.105.733z"/>
|
|
14
|
+
<path d="m13.398 12.207.07.349-.21.279-.21-.07-1.187-.838-1.815-1.606-1.397-.978.419-1.326.698.419.42.768z"/>
|
|
15
|
+
<path d="m12.49 8.645 1.746.14.419.28.279.418v.302l-.768.327-3.911-.978-1.606-.07.419-1.466 1.117.838z"/>
|
|
16
|
+
</g>
|
|
17
|
+
</svg>`;
|
|
18
|
+
export const claudeIcon = new LabIcon({
|
|
19
|
+
name: 'jupyterlab_claude_code_extension:claude',
|
|
20
|
+
svgstr: claudeSvgStr
|
|
21
|
+
});
|
|
22
|
+
const starFilledSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
|
|
23
|
+
<path class="jp-icon3" fill="#616161" d="M12 2.5l2.9 6.4 7 .7-5.3 4.7 1.6 6.9L12 17.7l-6.2 3.5 1.6-6.9-5.3-4.7 7-.7z"/>
|
|
24
|
+
</svg>`;
|
|
25
|
+
export const starFilledIcon = new LabIcon({
|
|
26
|
+
name: 'jupyterlab_claude_code_extension:star-filled',
|
|
27
|
+
svgstr: starFilledSvgStr
|
|
28
|
+
});
|
|
29
|
+
// Material-style icons matched verbatim to jupyterlab_trash_mgmt_extension so
|
|
30
|
+
// header/menu icons render at identical size and theme.
|
|
31
|
+
const refreshSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
|
|
32
|
+
<path class="jp-icon3" fill="#616161" d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
|
33
|
+
</svg>`;
|
|
34
|
+
export const refreshIcon = new LabIcon({
|
|
35
|
+
name: 'jupyterlab_claude_code_extension:refresh',
|
|
36
|
+
svgstr: refreshSvgStr
|
|
37
|
+
});
|
|
38
|
+
const removeSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
|
|
39
|
+
<path class="jp-icon3" fill="#616161" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM8 9h8v10H8V9zm7.5-5l-1-1h-5l-1 1H5v2h14V4h-3.5z"/>
|
|
40
|
+
</svg>`;
|
|
41
|
+
export const removeIcon = new LabIcon({
|
|
42
|
+
name: 'jupyterlab_claude_code_extension:remove',
|
|
43
|
+
svgstr: removeSvgStr
|
|
44
|
+
});
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ILabShell, ILayoutRestorer } from '@jupyterlab/application';
|
|
2
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
3
|
+
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
4
|
+
import { requestAPI } from './request';
|
|
5
|
+
import { ClaudeCodeSessionsWidget } from './widget';
|
|
6
|
+
const PLUGIN_ID = 'jupyterlab_claude_code_extension:plugin';
|
|
7
|
+
const WIDGET_ID = 'jupyterlab-claude-code-extension';
|
|
8
|
+
const plugin = {
|
|
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.',
|
|
11
|
+
autoStart: true,
|
|
12
|
+
requires: [ILabShell],
|
|
13
|
+
optional: [ILayoutRestorer, ISettingRegistry, ITerminalTracker],
|
|
14
|
+
activate: async (app, labShell, restorer, settingRegistry, terminalTracker) => {
|
|
15
|
+
const settings = app.serviceManager.serverSettings;
|
|
16
|
+
let status;
|
|
17
|
+
try {
|
|
18
|
+
status = await requestAPI('status', settings);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
console.error('[jupyterlab_claude_code_extension] status check failed; panel will not be registered.', err);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!status.enabled) {
|
|
25
|
+
console.info('[jupyterlab_claude_code_extension] `claude` binary not found on PATH; panel disabled.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const widget = new ClaudeCodeSessionsWidget(app, status.root_dir || '', terminalTracker);
|
|
29
|
+
labShell.add(widget, 'left', { rank: 600 });
|
|
30
|
+
if (settingRegistry) {
|
|
31
|
+
try {
|
|
32
|
+
const settings = await settingRegistry.load(PLUGIN_ID);
|
|
33
|
+
const apply = () => {
|
|
34
|
+
const resolve = settings.get('resolveSessionNames')
|
|
35
|
+
.composite;
|
|
36
|
+
widget.setResolveSessionNames(resolve !== false);
|
|
37
|
+
};
|
|
38
|
+
apply();
|
|
39
|
+
settings.changed.connect(apply);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.warn('[jupyterlab_claude_code_extension] failed to load settings; using defaults', err);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Register with the layout restorer so JL remembers whether the panel
|
|
46
|
+
// was active/visible across browser reloads and restarts.
|
|
47
|
+
if (restorer) {
|
|
48
|
+
restorer.add(widget, WIDGET_ID);
|
|
49
|
+
}
|
|
50
|
+
app.commands.addCommand('claude-code-sessions:refresh', {
|
|
51
|
+
label: 'Refresh Claude Code Sessions',
|
|
52
|
+
execute: () => widget.refresh()
|
|
53
|
+
});
|
|
54
|
+
console.log('[jupyterlab_claude_code_extension] panel registered (claude:', status.claude_path, ')');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export default plugin;
|
package/lib/request.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
2
|
+
/**
|
|
3
|
+
* Call the server extension
|
|
4
|
+
*
|
|
5
|
+
* @param endPoint API REST end point for the extension
|
|
6
|
+
* @param serverSettings The server settings to use for the request
|
|
7
|
+
* @param init Initial values for the request
|
|
8
|
+
* @returns The response body interpreted as JSON
|
|
9
|
+
*/
|
|
10
|
+
export declare function requestAPI<T>(endPoint: string, serverSettings: ServerConnection.ISettings, init?: RequestInit): Promise<T>;
|
package/lib/request.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { URLExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
3
|
+
/**
|
|
4
|
+
* Call the server extension
|
|
5
|
+
*
|
|
6
|
+
* @param endPoint API REST end point for the extension
|
|
7
|
+
* @param serverSettings The server settings to use for the request
|
|
8
|
+
* @param init Initial values for the request
|
|
9
|
+
* @returns The response body interpreted as JSON
|
|
10
|
+
*/
|
|
11
|
+
export async function requestAPI(endPoint, serverSettings, init = {}) {
|
|
12
|
+
// Make request to Jupyter API
|
|
13
|
+
const requestUrl = URLExt.join(serverSettings.baseUrl, 'jupyterlab-claude-code-extension', // our server extension's API namespace
|
|
14
|
+
endPoint);
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await ServerConnection.makeRequest(requestUrl, init, serverSettings);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new ServerConnection.NetworkError(error);
|
|
21
|
+
}
|
|
22
|
+
let data = await response.text();
|
|
23
|
+
if (data.length > 0) {
|
|
24
|
+
try {
|
|
25
|
+
data = JSON.parse(data);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.log('Not a JSON response body.', response);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new ServerConnection.ResponseError(response, data.message || data);
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface ISession {
|
|
2
|
+
project_path: string;
|
|
3
|
+
encoded_path: string;
|
|
4
|
+
session_id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
summary: string;
|
|
7
|
+
first_prompt: string;
|
|
8
|
+
message_count: number;
|
|
9
|
+
created: string | null;
|
|
10
|
+
modified: string | null;
|
|
11
|
+
file_mtime: number;
|
|
12
|
+
git_branch: string | null;
|
|
13
|
+
remote_control: boolean;
|
|
14
|
+
favourite: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ISessionsListResponse {
|
|
17
|
+
sessions: ISession[];
|
|
18
|
+
}
|
|
19
|
+
export interface IStatusResponse {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
claude_path: string | null;
|
|
22
|
+
root_dir: string;
|
|
23
|
+
}
|
|
24
|
+
export interface IFavouriteRequest {
|
|
25
|
+
project_path: string;
|
|
26
|
+
favourite: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface IFavouriteResponse {
|
|
29
|
+
favourites: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface IRemoveRequest {
|
|
32
|
+
encoded_path: string;
|
|
33
|
+
}
|
|
34
|
+
export interface IRemoveResponse {
|
|
35
|
+
removed: string;
|
|
36
|
+
}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/widget.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
|
+
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
3
|
+
import { Widget } from '@lumino/widgets';
|
|
4
|
+
import { Message } from '@lumino/messaging';
|
|
5
|
+
export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
6
|
+
constructor(app: JupyterFrontEnd, rootDir: string, terminalTracker?: ITerminalTracker | null);
|
|
7
|
+
refresh(): void;
|
|
8
|
+
/** Toggle whether explicit ``/rename`` names are honoured. */
|
|
9
|
+
setResolveSessionNames(on: boolean): void;
|
|
10
|
+
protected onAfterShow(_msg: Message): void;
|
|
11
|
+
protected onBeforeHide(_msg: Message): void;
|
|
12
|
+
protected onCloseRequest(msg: Message): void;
|
|
13
|
+
private _buildShell;
|
|
14
|
+
private _showLoading;
|
|
15
|
+
private _showError;
|
|
16
|
+
private _fetch;
|
|
17
|
+
private _toggleFavourite;
|
|
18
|
+
private _remove;
|
|
19
|
+
private _resumeInTerminal;
|
|
20
|
+
private _doResumeInTerminal;
|
|
21
|
+
private _shellQuote;
|
|
22
|
+
private _findTerminalForCwd;
|
|
23
|
+
private _wireTerminalDisposal;
|
|
24
|
+
/** Apply the resolve-names setting + path-segment disambiguation. */
|
|
25
|
+
private _displayName;
|
|
26
|
+
private _basename;
|
|
27
|
+
/** Walk path tails until every name in a colliding group is unique. */
|
|
28
|
+
private _disambiguate;
|
|
29
|
+
private _render;
|
|
30
|
+
private _renderSection;
|
|
31
|
+
private _renderRow;
|
|
32
|
+
private _lookupName;
|
|
33
|
+
private _buildRowTooltip;
|
|
34
|
+
private _displayPath;
|
|
35
|
+
private _formatRelativeTime;
|
|
36
|
+
private _setRefreshSpinning;
|
|
37
|
+
private _setActiveRow;
|
|
38
|
+
private _setupContextMenu;
|
|
39
|
+
private _startPolling;
|
|
40
|
+
private _stopPolling;
|
|
41
|
+
private readonly _app;
|
|
42
|
+
private readonly _serverSettings;
|
|
43
|
+
private _bodyEl;
|
|
44
|
+
private _statusEl;
|
|
45
|
+
private _refreshBtn;
|
|
46
|
+
private _sessions;
|
|
47
|
+
private _expanded;
|
|
48
|
+
private _commands;
|
|
49
|
+
private _contextMenu;
|
|
50
|
+
private _activeSession;
|
|
51
|
+
private _activeRowEl;
|
|
52
|
+
private _pollHandle;
|
|
53
|
+
private readonly _removingPaths;
|
|
54
|
+
private readonly _terminalTracker;
|
|
55
|
+
private readonly _terminalsByPath;
|
|
56
|
+
private readonly _pendingByPath;
|
|
57
|
+
private readonly _rootDir;
|
|
58
|
+
private _resolveNames;
|
|
59
|
+
private _displayNames;
|
|
60
|
+
}
|