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/package.json ADDED
@@ -0,0 +1,217 @@
1
+ {
2
+ "name": "jupyterlab_claude_code_extension",
3
+ "version": "1.0.16",
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
+ "keywords": [
6
+ "jupyter",
7
+ "jupyterlab",
8
+ "jupyterlab-extension"
9
+ ],
10
+ "homepage": "https://github.com/stellarshenson/jupyterlab_claude_code_extension",
11
+ "bugs": {
12
+ "url": "https://github.com/stellarshenson/jupyterlab_claude_code_extension/issues"
13
+ },
14
+ "license": "BSD-3-Clause",
15
+ "author": {
16
+ "name": "Stellars Henson",
17
+ "email": "konrad.jelen@gmail.com"
18
+ },
19
+ "files": [
20
+ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
21
+ "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
22
+ "src/**/*.{ts,tsx}"
23
+ ],
24
+ "main": "lib/index.js",
25
+ "types": "lib/index.d.ts",
26
+ "style": "style/index.css",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/stellarshenson/jupyterlab_claude_code_extension.git"
30
+ },
31
+ "scripts": {
32
+ "build": "jlpm build:lib && jlpm build:labextension:dev",
33
+ "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
34
+ "build:labextension": "jupyter labextension build .",
35
+ "build:labextension:dev": "jupyter labextension build --development True .",
36
+ "build:lib": "tsc --sourceMap",
37
+ "build:lib:prod": "tsc",
38
+ "clean": "jlpm clean:lib",
39
+ "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
40
+ "clean:lintcache": "rimraf .eslintcache .stylelintcache",
41
+ "clean:labextension": "rimraf jupyterlab_claude_code_extension/labextension jupyterlab_claude_code_extension/_version.py",
42
+ "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
43
+ "eslint": "jlpm eslint:check --fix",
44
+ "eslint:check": "eslint . --cache --ext .ts,.tsx",
45
+ "install:extension": "jlpm build",
46
+ "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
47
+ "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
48
+ "prettier": "jlpm prettier:base --write --list-different",
49
+ "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
50
+ "prettier:check": "jlpm prettier:base --check",
51
+ "stylelint": "jlpm stylelint:check --fix",
52
+ "stylelint:check": "stylelint --cache \"style/**/*.css\"",
53
+ "test": "jest --coverage",
54
+ "watch": "run-p watch:src watch:labextension",
55
+ "watch:src": "tsc -w --sourceMap",
56
+ "watch:labextension": "jupyter labextension watch ."
57
+ },
58
+ "dependencies": {
59
+ "@jupyterlab/application": "^4.0.0",
60
+ "@jupyterlab/apputils": "^4.0.0",
61
+ "@jupyterlab/coreutils": "^6.0.0",
62
+ "@jupyterlab/services": "^7.0.0",
63
+ "@jupyterlab/settingregistry": "^4.0.0",
64
+ "@jupyterlab/terminal": "^4.0.0",
65
+ "@jupyterlab/ui-components": "^4.0.0",
66
+ "@lumino/commands": "^2.0.0",
67
+ "@lumino/widgets": "^2.0.0"
68
+ },
69
+ "devDependencies": {
70
+ "@jupyterlab/builder": "^4.0.0",
71
+ "@jupyterlab/testutils": "^4.0.0",
72
+ "@types/jest": "^29.2.0",
73
+ "@types/json-schema": "^7.0.11",
74
+ "@types/react": "^18.0.26",
75
+ "@types/react-addons-linked-state-mixin": "^0.14.22",
76
+ "@typescript-eslint/eslint-plugin": "^6.1.0",
77
+ "@typescript-eslint/parser": "^6.1.0",
78
+ "css-loader": "^6.7.1",
79
+ "eslint": "^8.36.0",
80
+ "eslint-config-prettier": "^8.8.0",
81
+ "eslint-plugin-prettier": "^5.0.0",
82
+ "jest": "^29.2.0",
83
+ "mkdirp": "^1.0.3",
84
+ "npm-run-all2": "^7.0.1",
85
+ "prettier": "^3.0.0",
86
+ "rimraf": "^5.0.1",
87
+ "source-map-loader": "^1.0.2",
88
+ "style-loader": "^3.3.1",
89
+ "stylelint": "^15.10.1",
90
+ "stylelint-config-recommended": "^13.0.0",
91
+ "stylelint-config-standard": "^34.0.0",
92
+ "stylelint-csstree-validator": "^3.0.0",
93
+ "stylelint-prettier": "^4.0.0",
94
+ "typescript": "~5.5.4",
95
+ "yjs": "^13.5.0"
96
+ },
97
+ "resolutions": {
98
+ "lib0": "0.2.111"
99
+ },
100
+ "sideEffects": [
101
+ "style/*.css",
102
+ "style/index.js"
103
+ ],
104
+ "styleModule": "style/index.js",
105
+ "publishConfig": {
106
+ "access": "public"
107
+ },
108
+ "jupyterlab": {
109
+ "discovery": {
110
+ "server": {
111
+ "managers": [
112
+ "pip"
113
+ ],
114
+ "base": {
115
+ "name": "jupyterlab_claude_code_extension"
116
+ }
117
+ }
118
+ },
119
+ "extension": true,
120
+ "schemaDir": "schema",
121
+ "outputDir": "jupyterlab_claude_code_extension/labextension"
122
+ },
123
+ "eslintIgnore": [
124
+ "node_modules",
125
+ "dist",
126
+ "coverage",
127
+ "**/*.d.ts",
128
+ "tests",
129
+ "**/__tests__",
130
+ "ui-tests"
131
+ ],
132
+ "eslintConfig": {
133
+ "extends": [
134
+ "eslint:recommended",
135
+ "plugin:@typescript-eslint/eslint-recommended",
136
+ "plugin:@typescript-eslint/recommended",
137
+ "plugin:prettier/recommended"
138
+ ],
139
+ "parser": "@typescript-eslint/parser",
140
+ "parserOptions": {
141
+ "project": "tsconfig.json",
142
+ "sourceType": "module"
143
+ },
144
+ "plugins": [
145
+ "@typescript-eslint"
146
+ ],
147
+ "rules": {
148
+ "@typescript-eslint/naming-convention": [
149
+ "error",
150
+ {
151
+ "selector": "interface",
152
+ "format": [
153
+ "PascalCase"
154
+ ],
155
+ "custom": {
156
+ "regex": "^I[A-Z]",
157
+ "match": true
158
+ }
159
+ }
160
+ ],
161
+ "@typescript-eslint/no-unused-vars": [
162
+ "warn",
163
+ {
164
+ "args": "none"
165
+ }
166
+ ],
167
+ "@typescript-eslint/no-explicit-any": "off",
168
+ "@typescript-eslint/no-namespace": "off",
169
+ "@typescript-eslint/no-use-before-define": "off",
170
+ "@typescript-eslint/quotes": [
171
+ "error",
172
+ "single",
173
+ {
174
+ "avoidEscape": true,
175
+ "allowTemplateLiterals": false
176
+ }
177
+ ],
178
+ "curly": [
179
+ "error",
180
+ "all"
181
+ ],
182
+ "eqeqeq": "error",
183
+ "prefer-arrow-callback": "error"
184
+ }
185
+ },
186
+ "prettier": {
187
+ "singleQuote": true,
188
+ "trailingComma": "none",
189
+ "arrowParens": "avoid",
190
+ "endOfLine": "auto",
191
+ "overrides": [
192
+ {
193
+ "files": "package.json",
194
+ "options": {
195
+ "tabWidth": 4
196
+ }
197
+ }
198
+ ]
199
+ },
200
+ "stylelint": {
201
+ "extends": [
202
+ "stylelint-config-recommended",
203
+ "stylelint-config-standard",
204
+ "stylelint-prettier/recommended"
205
+ ],
206
+ "plugins": [
207
+ "stylelint-csstree-validator"
208
+ ],
209
+ "rules": {
210
+ "csstree/validator": true,
211
+ "property-no-vendor-prefix": null,
212
+ "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
213
+ "selector-no-vendor-prefix": null,
214
+ "value-no-vendor-prefix": null
215
+ }
216
+ }
217
+ }
@@ -0,0 +1,62 @@
1
+ import type { ISession } from '../types';
2
+
3
+ const session = (over: Partial<ISession> = {}): ISession => ({
4
+ project_path: '/p',
5
+ encoded_path: '-p',
6
+ session_id: 'sid',
7
+ name: 'P',
8
+ summary: '',
9
+ first_prompt: '',
10
+ message_count: 0,
11
+ created: null,
12
+ modified: null,
13
+ file_mtime: 0,
14
+ git_branch: null,
15
+ remote_control: false,
16
+ favourite: false,
17
+ ...over
18
+ });
19
+
20
+ describe('session sorting', () => {
21
+ it('orders by file_mtime descending', () => {
22
+ const items = [
23
+ session({ project_path: 'a', file_mtime: 1 }),
24
+ session({ project_path: 'b', file_mtime: 3 }),
25
+ session({ project_path: 'c', file_mtime: 2 })
26
+ ];
27
+ const sorted = [...items].sort((a, b) => b.file_mtime - a.file_mtime);
28
+ expect(sorted.map(s => s.project_path)).toEqual(['b', 'c', 'a']);
29
+ });
30
+
31
+ it('orders alphabetically by display name', () => {
32
+ const items = [
33
+ session({ name: 'Charlie' }),
34
+ session({ name: 'alpha' }),
35
+ session({ name: 'Beta' })
36
+ ];
37
+ const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
38
+ expect(sorted.map(s => s.name)).toEqual(['alpha', 'Beta', 'Charlie']);
39
+ });
40
+
41
+ it('filters favourites only', () => {
42
+ const items = [
43
+ session({ project_path: 'a', favourite: true }),
44
+ session({ project_path: 'b', favourite: false }),
45
+ session({ project_path: 'c', favourite: true })
46
+ ];
47
+ const favs = items.filter(s => s.favourite).map(s => s.project_path);
48
+ expect(favs).toEqual(['a', 'c']);
49
+ });
50
+
51
+ it('caps recent at the configured limit', () => {
52
+ const items = Array.from({ length: 25 }, (_, i) =>
53
+ session({ project_path: `p${i}`, file_mtime: i })
54
+ );
55
+ const recent = [...items]
56
+ .sort((a, b) => b.file_mtime - a.file_mtime)
57
+ .slice(0, 10);
58
+ expect(recent).toHaveLength(10);
59
+ expect(recent[0].project_path).toBe('p24');
60
+ expect(recent[9].project_path).toBe('p15');
61
+ });
62
+ });
package/src/icons.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { LabIcon } from '@jupyterlab/ui-components';
2
+
3
+ const claudeSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
4
+ <g class="jp-icon3" fill="#616161">
5
+ <path d="m14.375 6.48.49.28v.209l-.14.489-5.937 1.397-.558-1.387zm0 0"/>
6
+ <path d="m12.155 2.373.683.143.182.224.173.535-.072.342-3.983 5.447L7.81 7.737l3.673-4.82z"/>
7
+ <path d="m8.719 1.522.419-.28.349.14.349.49-.957 5.748-.65-.441-.279-.769.49-4.33z"/>
8
+ <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"/>
9
+ <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"/>
10
+ <path d="M1.316 8.296 1 7.946v-.31l.316-.108 3.562.21 3.491.279-.113.695-6.66-.346z"/>
11
+ <path d="M3.411 11.931h-.698l-.278-.32v-.382l1.186-.838 4.82-3.068.487.833z"/>
12
+ <path d="m4.738 13.883-.28.07-.418-.21.07-.35 4.12-5.446.558.768-3.072 4.05z"/>
13
+ <path d="m8.23 14.581-.21.28-.419.14-.349-.28-.21-.42L8.09 8.646l.629.07z"/>
14
+ <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"/>
15
+ <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"/>
16
+ <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"/>
17
+ </g>
18
+ </svg>`;
19
+
20
+ export const claudeIcon = new LabIcon({
21
+ name: 'jupyterlab_claude_code_extension:claude',
22
+ svgstr: claudeSvgStr
23
+ });
24
+
25
+ const starFilledSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
26
+ <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"/>
27
+ </svg>`;
28
+
29
+ export const starFilledIcon = new LabIcon({
30
+ name: 'jupyterlab_claude_code_extension:star-filled',
31
+ svgstr: starFilledSvgStr
32
+ });
33
+
34
+ // Material-style icons matched verbatim to jupyterlab_trash_mgmt_extension so
35
+ // header/menu icons render at identical size and theme.
36
+ const refreshSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
37
+ <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"/>
38
+ </svg>`;
39
+
40
+ export const refreshIcon = new LabIcon({
41
+ name: 'jupyterlab_claude_code_extension:refresh',
42
+ svgstr: refreshSvgStr
43
+ });
44
+
45
+ const removeSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
46
+ <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"/>
47
+ </svg>`;
48
+
49
+ export const removeIcon = new LabIcon({
50
+ name: 'jupyterlab_claude_code_extension:remove',
51
+ svgstr: removeSvgStr
52
+ });
package/src/index.ts ADDED
@@ -0,0 +1,95 @@
1
+ import {
2
+ ILabShell,
3
+ ILayoutRestorer,
4
+ JupyterFrontEnd,
5
+ JupyterFrontEndPlugin
6
+ } from '@jupyterlab/application';
7
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
8
+ import { ITerminalTracker } from '@jupyterlab/terminal';
9
+
10
+ import { requestAPI } from './request';
11
+ import { IStatusResponse } from './types';
12
+ import { ClaudeCodeSessionsWidget } from './widget';
13
+
14
+ const PLUGIN_ID = 'jupyterlab_claude_code_extension:plugin';
15
+ const WIDGET_ID = 'jupyterlab-claude-code-extension';
16
+
17
+ const plugin: JupyterFrontEndPlugin<void> = {
18
+ id: PLUGIN_ID,
19
+ description:
20
+ 'Side panel listing Claude Code sessions per project folder, with remote-control indicator, favourites, and one-click resume in a terminal.',
21
+ autoStart: true,
22
+ requires: [ILabShell],
23
+ optional: [ILayoutRestorer, ISettingRegistry, ITerminalTracker],
24
+ activate: async (
25
+ app: JupyterFrontEnd,
26
+ labShell: ILabShell,
27
+ restorer: ILayoutRestorer | null,
28
+ settingRegistry: ISettingRegistry | null,
29
+ terminalTracker: ITerminalTracker | null
30
+ ) => {
31
+ const settings = app.serviceManager.serverSettings;
32
+
33
+ let status: IStatusResponse;
34
+ try {
35
+ status = await requestAPI<IStatusResponse>('status', settings);
36
+ } catch (err) {
37
+ console.error(
38
+ '[jupyterlab_claude_code_extension] status check failed; panel will not be registered.',
39
+ err
40
+ );
41
+ return;
42
+ }
43
+
44
+ if (!status.enabled) {
45
+ console.info(
46
+ '[jupyterlab_claude_code_extension] `claude` binary not found on PATH; panel disabled.'
47
+ );
48
+ return;
49
+ }
50
+
51
+ const widget = new ClaudeCodeSessionsWidget(
52
+ app,
53
+ status.root_dir || '',
54
+ terminalTracker
55
+ );
56
+ labShell.add(widget, 'left', { rank: 600 });
57
+
58
+ if (settingRegistry) {
59
+ try {
60
+ const settings = await settingRegistry.load(PLUGIN_ID);
61
+ const apply = (): void => {
62
+ const resolve = settings.get('resolveSessionNames')
63
+ .composite as boolean;
64
+ widget.setResolveSessionNames(resolve !== false);
65
+ };
66
+ apply();
67
+ settings.changed.connect(apply);
68
+ } catch (err) {
69
+ console.warn(
70
+ '[jupyterlab_claude_code_extension] failed to load settings; using defaults',
71
+ err
72
+ );
73
+ }
74
+ }
75
+
76
+ // Register with the layout restorer so JL remembers whether the panel
77
+ // was active/visible across browser reloads and restarts.
78
+ if (restorer) {
79
+ restorer.add(widget, WIDGET_ID);
80
+ }
81
+
82
+ app.commands.addCommand('claude-code-sessions:refresh', {
83
+ label: 'Refresh Claude Code Sessions',
84
+ execute: () => widget.refresh()
85
+ });
86
+
87
+ console.log(
88
+ '[jupyterlab_claude_code_extension] panel registered (claude:',
89
+ status.claude_path,
90
+ ')'
91
+ );
92
+ }
93
+ };
94
+
95
+ export default plugin;
package/src/request.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { URLExt } from '@jupyterlab/coreutils';
2
+
3
+ import { ServerConnection } from '@jupyterlab/services';
4
+
5
+ /**
6
+ * Call the server extension
7
+ *
8
+ * @param endPoint API REST end point for the extension
9
+ * @param serverSettings The server settings to use for the request
10
+ * @param init Initial values for the request
11
+ * @returns The response body interpreted as JSON
12
+ */
13
+ export async function requestAPI<T>(
14
+ endPoint: string,
15
+ serverSettings: ServerConnection.ISettings,
16
+ init: RequestInit = {}
17
+ ): Promise<T> {
18
+ // Make request to Jupyter API
19
+ const requestUrl = URLExt.join(
20
+ serverSettings.baseUrl,
21
+ 'jupyterlab-claude-code-extension', // our server extension's API namespace
22
+ endPoint
23
+ );
24
+
25
+ let response: Response;
26
+ try {
27
+ response = await ServerConnection.makeRequest(
28
+ requestUrl,
29
+ init,
30
+ serverSettings
31
+ );
32
+ } catch (error) {
33
+ throw new ServerConnection.NetworkError(error as any);
34
+ }
35
+
36
+ let data: any = await response.text();
37
+
38
+ if (data.length > 0) {
39
+ try {
40
+ data = JSON.parse(data);
41
+ } catch (error) {
42
+ console.log('Not a JSON response body.', response);
43
+ }
44
+ }
45
+
46
+ if (!response.ok) {
47
+ throw new ServerConnection.ResponseError(response, data.message || data);
48
+ }
49
+
50
+ return data;
51
+ }
package/src/types.ts ADDED
@@ -0,0 +1,42 @@
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
+
17
+ export interface ISessionsListResponse {
18
+ sessions: ISession[];
19
+ }
20
+
21
+ export interface IStatusResponse {
22
+ enabled: boolean;
23
+ claude_path: string | null;
24
+ root_dir: string;
25
+ }
26
+
27
+ export interface IFavouriteRequest {
28
+ project_path: string;
29
+ favourite: boolean;
30
+ }
31
+
32
+ export interface IFavouriteResponse {
33
+ favourites: string[];
34
+ }
35
+
36
+ export interface IRemoveRequest {
37
+ encoded_path: string;
38
+ }
39
+
40
+ export interface IRemoveResponse {
41
+ removed: string;
42
+ }