jupyterlab_claude_code_extension 1.2.41 → 1.2.43
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 +1 -0
- package/lib/index.js +5 -3
- package/lib/types.d.ts +3 -0
- package/lib/widget.d.ts +35 -1
- package/lib/widget.js +118 -1
- package/package.json +10 -3
- package/src/__tests__/jupyterlab_claude_code_extension.spec.ts +1 -0
- package/src/index.ts +7 -3
- package/src/types.ts +3 -0
- package/src/widget.ts +141 -1
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
36
36
|
- **Branch session** - fork the current conversation into a new named session via the right-click menu (normal or skip-permissions mode); uses Claude's native `--fork-session`, opens in a new terminal, the chosen name is stamped automatically, and the fork becomes the row's current conversation
|
|
37
37
|
- **Copy session id** - a right-click "Copy Session ID" item copies the row's current conversation id to the clipboard; the Manage Sessions popup adds a copy button on every row, so any parallel conversation's id is one click away
|
|
38
38
|
- **Activity at a glance** - each row shows its last activity (`now`, `5m ago`, `2h ago`, `3d ago`) in an aligned column, with the favourite star in its own column beside it; rows active within the last minute light up in the theme's brand colour (including the `now` label), rows idle for over a week dim slightly
|
|
39
|
+
- **Coloured terminal tabs** - each Claude session's colour (set with `/color`, or auto-assigned) tints its terminal tab, so open sessions are easy to tell apart; needs the companion `jupyterlab_colourful_tab_extension` (>= 1.0.19), which supplies the tab-colouring API
|
|
39
40
|
- **Search** - fuzzy filter toggled by the funnel button next to refresh
|
|
40
41
|
- **Presentation modes** - label rows by session name (so a `/rename` shows through), folder name, or path relative to the JupyterLab root
|
|
41
42
|
- **Hover tooltip** with project path, last activity, message count, branch, and session id
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ILabShell, ILayoutRestorer } from '@jupyterlab/application';
|
|
|
2
2
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
3
3
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
4
4
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
5
|
+
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
5
6
|
import { requestAPI } from './request';
|
|
6
7
|
import { ClaudeCodeSessionsWidget } from './widget';
|
|
7
8
|
const PLUGIN_ID = 'jupyterlab_claude_code_extension:plugin';
|
|
@@ -15,9 +16,10 @@ const plugin = {
|
|
|
15
16
|
ILayoutRestorer,
|
|
16
17
|
ISettingRegistry,
|
|
17
18
|
ITerminalTracker,
|
|
18
|
-
IDefaultFileBrowser
|
|
19
|
+
IDefaultFileBrowser,
|
|
20
|
+
IColourfulTabs
|
|
19
21
|
],
|
|
20
|
-
activate: async (app, labShell, restorer, settingRegistry, terminalTracker, fileBrowser) => {
|
|
22
|
+
activate: async (app, labShell, restorer, settingRegistry, terminalTracker, fileBrowser, colourfulTabs) => {
|
|
21
23
|
const settings = app.serviceManager.serverSettings;
|
|
22
24
|
let status;
|
|
23
25
|
try {
|
|
@@ -31,7 +33,7 @@ const plugin = {
|
|
|
31
33
|
console.info('[jupyterlab_claude_code_extension] `claude` binary not found on PATH; panel disabled.');
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
|
-
const widget = new ClaudeCodeSessionsWidget(app, status.root_dir || '', terminalTracker, fileBrowser);
|
|
36
|
+
const widget = new ClaudeCodeSessionsWidget(app, status.root_dir || '', terminalTracker, fileBrowser, colourfulTabs);
|
|
35
37
|
// Read the sidebar setting before docking so we add the widget to the
|
|
36
38
|
// user's preferred side on first paint. Default to right.
|
|
37
39
|
let currentSidebar = 'right';
|
package/lib/types.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface ISession {
|
|
|
14
14
|
remote_control: boolean;
|
|
15
15
|
favourite: boolean;
|
|
16
16
|
extra_sessions: number;
|
|
17
|
+
/** Claude session colour from `/color` or auto-assignment (e.g. "blue"), or
|
|
18
|
+
* null when the session carries none. Drives the terminal tab tint. */
|
|
19
|
+
color: string | null;
|
|
17
20
|
}
|
|
18
21
|
export interface ISessionsListResponse {
|
|
19
22
|
sessions: ISession[];
|
package/lib/widget.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
2
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
3
3
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
4
|
+
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
4
5
|
import { Widget } from '@lumino/widgets';
|
|
5
6
|
import { Message } from '@lumino/messaging';
|
|
6
7
|
export type PresentationMode = 'name' | 'path';
|
|
7
8
|
export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
8
|
-
constructor(app: JupyterFrontEnd, rootDir: string, terminalTracker?: ITerminalTracker | null, fileBrowser?: IDefaultFileBrowser | null);
|
|
9
|
+
constructor(app: JupyterFrontEnd, rootDir: string, terminalTracker?: ITerminalTracker | null, fileBrowser?: IDefaultFileBrowser | null, colourfulTabs?: IColourfulTabs | null);
|
|
9
10
|
refresh(): void;
|
|
10
11
|
/** Choose how rows are labelled: by name (session name, initially the
|
|
11
12
|
* folder name), or by path. */
|
|
@@ -81,6 +82,38 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
81
82
|
* first.
|
|
82
83
|
*/
|
|
83
84
|
private _focusTerminal;
|
|
85
|
+
/** Tint a terminal's dock tab with the colour of the Claude session it runs,
|
|
86
|
+
* delegating to jupyterlab_colourful_tab_extension's `setColour` API so that
|
|
87
|
+
* extension owns the tab CSS and colour vocabulary. A no-op when the
|
|
88
|
+
* colourful-tab extension is not installed (the token is optional). An
|
|
89
|
+
* absent/unknown colour clears the tint. */
|
|
90
|
+
private _applyTerminalColour;
|
|
91
|
+
/** Re-tint EVERY open Claude terminal tab from the freshest session colours,
|
|
92
|
+
* whether the plugin launched it or the user opened it themselves. Walks
|
|
93
|
+
* JupyterLab's terminal tracker (the registry of all open terminals) rather
|
|
94
|
+
* than only the plugin's own launch cache, and re-resolves each terminal's
|
|
95
|
+
* Claude conversation on every pass via the ``terminal-cwd`` probe - so a tab
|
|
96
|
+
* whose terminal has since started (or switched) a Claude conversation is
|
|
97
|
+
* re-tinted correctly rather than pinned to a stale identity. The probe is a
|
|
98
|
+
* few /proc reads per terminal; runs after each fetch (launch refresh + the
|
|
99
|
+
* 30s poll), which is already gated to skip while a context menu is open. */
|
|
100
|
+
private _reconcileTerminalColours;
|
|
101
|
+
/** Resolve a terminal's Claude identity: does it run Claude, which
|
|
102
|
+
* conversation id, and its cwd(s). Panel-launched terminals are already known
|
|
103
|
+
* (their conversation was confirmed at launch), so those skip the backend
|
|
104
|
+
* probe; every other terminal is probed once via ``terminal-cwd``. Returns
|
|
105
|
+
* ``null`` when the terminal has no session name yet or the probe fails. */
|
|
106
|
+
private _interrogateTerminal;
|
|
107
|
+
/** Pick a terminal's tab colour from the session list. When the terminal runs
|
|
108
|
+
* a known conversation (its argv carried a ``--resume`` / ``--session-id``),
|
|
109
|
+
* match on that id ONLY: it takes the colour of the exact conversation, or -
|
|
110
|
+
* when that conversation is not the project's representative row - clears,
|
|
111
|
+
* rather than borrowing a sibling branch's colour. The cwd fallback is used
|
|
112
|
+
* ONLY for a terminal with no conversation id (a bare ``claude`` / ``-c``),
|
|
113
|
+
* matching the project the terminal is cwd'd in and preferring the longest
|
|
114
|
+
* matching path so a nested project wins over its parent. Returns null (clear)
|
|
115
|
+
* when no row matches or the matched row has no colour. */
|
|
116
|
+
private _colourForTerminal;
|
|
84
117
|
private _findTerminalForCwd;
|
|
85
118
|
private _showCloseExistingDialog;
|
|
86
119
|
/** Show a modal with a spinner while the terminal is being launched. The
|
|
@@ -204,6 +237,7 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
204
237
|
private readonly _removingPaths;
|
|
205
238
|
private readonly _terminalTracker;
|
|
206
239
|
private readonly _fileBrowser;
|
|
240
|
+
private readonly _colourfulTabs;
|
|
207
241
|
private readonly _terminalsByPath;
|
|
208
242
|
private readonly _pendingByPath;
|
|
209
243
|
private readonly _rootDir;
|
package/lib/widget.js
CHANGED
|
@@ -14,6 +14,21 @@ const BRANCH_WATCH_MAX_ATTEMPTS = 90; // ~3 minutes
|
|
|
14
14
|
const DEFAULT_RECENT_LIMIT = 10;
|
|
15
15
|
const EXPANDED_STORAGE_KEY = 'jupyterlab_claude_code_extension:expanded';
|
|
16
16
|
const DEFAULT_PRESENTATION_MODE = 'name';
|
|
17
|
+
// Claude's per-session colour (set by `/color`, or auto-assigned for
|
|
18
|
+
// multi-session use) maps to a jupyterlab_colourful_tab_extension colour id.
|
|
19
|
+
// That extension owns the tab-tint CSS and the setColour API; we only feed it
|
|
20
|
+
// the colour. An absent or unrecognised colour resolves to null (clear tint).
|
|
21
|
+
const CLAUDE_TAB_COLOUR_ID = {
|
|
22
|
+
red: 'rose',
|
|
23
|
+
orange: 'peach',
|
|
24
|
+
yellow: 'lemon',
|
|
25
|
+
green: 'mint',
|
|
26
|
+
blue: 'sky',
|
|
27
|
+
purple: 'lavender'
|
|
28
|
+
};
|
|
29
|
+
function claudeTabColourId(color) {
|
|
30
|
+
return (color && CLAUDE_TAB_COLOUR_ID[color]) || null;
|
|
31
|
+
}
|
|
17
32
|
const SECTION_LABELS = {
|
|
18
33
|
favourites: 'Favorites',
|
|
19
34
|
recent: 'Recent',
|
|
@@ -62,7 +77,7 @@ catch (_err) {
|
|
|
62
77
|
// ignore
|
|
63
78
|
}
|
|
64
79
|
export class ClaudeCodeSessionsWidget extends Widget {
|
|
65
|
-
constructor(app, rootDir, terminalTracker = null, fileBrowser = null) {
|
|
80
|
+
constructor(app, rootDir, terminalTracker = null, fileBrowser = null, colourfulTabs = null) {
|
|
66
81
|
super();
|
|
67
82
|
this._loadingEl = null;
|
|
68
83
|
this._refreshBtn = null;
|
|
@@ -92,6 +107,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
92
107
|
this._rootDir = rootDir.replace(/\/+$/, '');
|
|
93
108
|
this._terminalTracker = terminalTracker;
|
|
94
109
|
this._fileBrowser = fileBrowser;
|
|
110
|
+
this._colourfulTabs = colourfulTabs;
|
|
95
111
|
this.id = 'jupyterlab-claude-code-extension';
|
|
96
112
|
this.title.icon = claudeIcon;
|
|
97
113
|
this.title.caption = 'Claude Code Sessions';
|
|
@@ -337,6 +353,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
337
353
|
const data = await requestAPI('sessions', this._serverSettings, { cache: 'no-store' });
|
|
338
354
|
this._sessions = (_a = data.sessions) !== null && _a !== void 0 ? _a : [];
|
|
339
355
|
this._render();
|
|
356
|
+
// Best-effort tab tinting; never let a colour failure break the fetch.
|
|
357
|
+
this._reconcileTerminalColours().catch(() => undefined);
|
|
340
358
|
}
|
|
341
359
|
async _toggleFavourite(session) {
|
|
342
360
|
const next = !session.favourite;
|
|
@@ -641,6 +659,105 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
641
659
|
}
|
|
642
660
|
});
|
|
643
661
|
}
|
|
662
|
+
/** Tint a terminal's dock tab with the colour of the Claude session it runs,
|
|
663
|
+
* delegating to jupyterlab_colourful_tab_extension's `setColour` API so that
|
|
664
|
+
* extension owns the tab CSS and colour vocabulary. A no-op when the
|
|
665
|
+
* colourful-tab extension is not installed (the token is optional). An
|
|
666
|
+
* absent/unknown colour clears the tint. */
|
|
667
|
+
_applyTerminalColour(widget, color) {
|
|
668
|
+
if (!this._colourfulTabs || !widget || widget.isDisposed) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
this._colourfulTabs.setColour(widget, claudeTabColourId(color));
|
|
672
|
+
}
|
|
673
|
+
/** Re-tint EVERY open Claude terminal tab from the freshest session colours,
|
|
674
|
+
* whether the plugin launched it or the user opened it themselves. Walks
|
|
675
|
+
* JupyterLab's terminal tracker (the registry of all open terminals) rather
|
|
676
|
+
* than only the plugin's own launch cache, and re-resolves each terminal's
|
|
677
|
+
* Claude conversation on every pass via the ``terminal-cwd`` probe - so a tab
|
|
678
|
+
* whose terminal has since started (or switched) a Claude conversation is
|
|
679
|
+
* re-tinted correctly rather than pinned to a stale identity. The probe is a
|
|
680
|
+
* few /proc reads per terminal; runs after each fetch (launch refresh + the
|
|
681
|
+
* 30s poll), which is already gated to skip while a context menu is open. */
|
|
682
|
+
async _reconcileTerminalColours() {
|
|
683
|
+
var _a;
|
|
684
|
+
if (!this._colourfulTabs || !this._terminalTracker) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const sessions = (_a = this._sessions) !== null && _a !== void 0 ? _a : [];
|
|
688
|
+
const widgets = [];
|
|
689
|
+
this._terminalTracker.forEach((widget) => {
|
|
690
|
+
if (widget && !widget.isDisposed) {
|
|
691
|
+
widgets.push(widget);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
await Promise.all(widgets.map(async (widget) => {
|
|
695
|
+
const info = await this._interrogateTerminal(widget);
|
|
696
|
+
if (!info || !info.hasClaude || widget.isDisposed) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
this._applyTerminalColour(widget, this._colourForTerminal(info, sessions));
|
|
700
|
+
}));
|
|
701
|
+
}
|
|
702
|
+
/** Resolve a terminal's Claude identity: does it run Claude, which
|
|
703
|
+
* conversation id, and its cwd(s). Panel-launched terminals are already known
|
|
704
|
+
* (their conversation was confirmed at launch), so those skip the backend
|
|
705
|
+
* probe; every other terminal is probed once via ``terminal-cwd``. Returns
|
|
706
|
+
* ``null`` when the terminal has no session name yet or the probe fails. */
|
|
707
|
+
async _interrogateTerminal(widget) {
|
|
708
|
+
var _a, _b, _c;
|
|
709
|
+
for (const entry of this._terminalsByPath.values()) {
|
|
710
|
+
if (entry.widget === widget && entry.sessionId) {
|
|
711
|
+
return { hasClaude: true, sessionId: entry.sessionId, cwds: [] };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const sessName = (_b = (_a = widget === null || widget === void 0 ? void 0 : widget.content) === null || _a === void 0 ? void 0 : _a.session) === null || _b === void 0 ? void 0 : _b.name;
|
|
715
|
+
if (typeof sessName !== 'string' || !sessName) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
const data = await requestAPI(`terminal-cwd/${encodeURIComponent(sessName)}`, this._serverSettings);
|
|
720
|
+
return {
|
|
721
|
+
hasClaude: !!(data === null || data === void 0 ? void 0 : data.has_claude),
|
|
722
|
+
sessionId: (_c = data === null || data === void 0 ? void 0 : data.session_id) !== null && _c !== void 0 ? _c : null,
|
|
723
|
+
cwds: Array.isArray(data === null || data === void 0 ? void 0 : data.cwds) ? data.cwds : []
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
catch (_err) {
|
|
727
|
+
// Backend may 404 for a terminal that vanished between enumeration and
|
|
728
|
+
// probe - treat as unresolved and retry on the next poll.
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/** Pick a terminal's tab colour from the session list. When the terminal runs
|
|
733
|
+
* a known conversation (its argv carried a ``--resume`` / ``--session-id``),
|
|
734
|
+
* match on that id ONLY: it takes the colour of the exact conversation, or -
|
|
735
|
+
* when that conversation is not the project's representative row - clears,
|
|
736
|
+
* rather than borrowing a sibling branch's colour. The cwd fallback is used
|
|
737
|
+
* ONLY for a terminal with no conversation id (a bare ``claude`` / ``-c``),
|
|
738
|
+
* matching the project the terminal is cwd'd in and preferring the longest
|
|
739
|
+
* matching path so a nested project wins over its parent. Returns null (clear)
|
|
740
|
+
* when no row matches or the matched row has no colour. */
|
|
741
|
+
_colourForTerminal(info, sessions) {
|
|
742
|
+
var _a, _b;
|
|
743
|
+
if (info.sessionId) {
|
|
744
|
+
const bySession = sessions.find(s => s.session_id === info.sessionId);
|
|
745
|
+
return bySession ? ((_a = bySession.color) !== null && _a !== void 0 ? _a : null) : null;
|
|
746
|
+
}
|
|
747
|
+
let best;
|
|
748
|
+
let bestLen = -1;
|
|
749
|
+
for (const raw of info.cwds) {
|
|
750
|
+
const cwd = raw.replace(/\/+$/, '');
|
|
751
|
+
for (const s of sessions) {
|
|
752
|
+
const p = s.project_path.replace(/\/+$/, '');
|
|
753
|
+
if ((cwd === p || cwd.startsWith(p + '/')) && p.length > bestLen) {
|
|
754
|
+
best = s;
|
|
755
|
+
bestLen = p.length;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return best ? ((_b = best.color) !== null && _b !== void 0 ? _b : null) : null;
|
|
760
|
+
}
|
|
644
761
|
async _findTerminalForCwd(projectPath, wantedSessionId) {
|
|
645
762
|
var _a, _b, _c;
|
|
646
763
|
if (!this._terminalTracker) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_claude_code_extension",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.43",
|
|
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",
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
"@jupyterlab/terminal": "^4.0.0",
|
|
65
65
|
"@jupyterlab/ui-components": "^4.0.0",
|
|
66
66
|
"@lumino/commands": "^2.0.0",
|
|
67
|
-
"@lumino/widgets": "^2.0.0"
|
|
67
|
+
"@lumino/widgets": "^2.0.0",
|
|
68
|
+
"jupyterlab_colourful_tab_extension": "^1.0.19"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
71
|
"@jupyterlab/builder": "^4.0.0",
|
|
@@ -125,7 +126,13 @@
|
|
|
125
126
|
},
|
|
126
127
|
"extension": true,
|
|
127
128
|
"schemaDir": "schema",
|
|
128
|
-
"outputDir": "jupyterlab_claude_code_extension/labextension"
|
|
129
|
+
"outputDir": "jupyterlab_claude_code_extension/labextension",
|
|
130
|
+
"sharedPackages": {
|
|
131
|
+
"jupyterlab_colourful_tab_extension": {
|
|
132
|
+
"bundled": false,
|
|
133
|
+
"singleton": true
|
|
134
|
+
}
|
|
135
|
+
}
|
|
129
136
|
},
|
|
130
137
|
"eslintIgnore": [
|
|
131
138
|
"node_modules",
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
8
8
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
9
9
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
10
|
+
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
10
11
|
|
|
11
12
|
import { requestAPI } from './request';
|
|
12
13
|
import { IStatusResponse } from './types';
|
|
@@ -25,7 +26,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
25
26
|
ILayoutRestorer,
|
|
26
27
|
ISettingRegistry,
|
|
27
28
|
ITerminalTracker,
|
|
28
|
-
IDefaultFileBrowser
|
|
29
|
+
IDefaultFileBrowser,
|
|
30
|
+
IColourfulTabs
|
|
29
31
|
],
|
|
30
32
|
activate: async (
|
|
31
33
|
app: JupyterFrontEnd,
|
|
@@ -33,7 +35,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
33
35
|
restorer: ILayoutRestorer | null,
|
|
34
36
|
settingRegistry: ISettingRegistry | null,
|
|
35
37
|
terminalTracker: ITerminalTracker | null,
|
|
36
|
-
fileBrowser: IDefaultFileBrowser | null
|
|
38
|
+
fileBrowser: IDefaultFileBrowser | null,
|
|
39
|
+
colourfulTabs: IColourfulTabs | null
|
|
37
40
|
) => {
|
|
38
41
|
const settings = app.serviceManager.serverSettings;
|
|
39
42
|
|
|
@@ -59,7 +62,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
59
62
|
app,
|
|
60
63
|
status.root_dir || '',
|
|
61
64
|
terminalTracker,
|
|
62
|
-
fileBrowser
|
|
65
|
+
fileBrowser,
|
|
66
|
+
colourfulTabs
|
|
63
67
|
);
|
|
64
68
|
|
|
65
69
|
// Read the sidebar setting before docking so we add the widget to the
|
package/src/types.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface ISession {
|
|
|
14
14
|
remote_control: boolean;
|
|
15
15
|
favourite: boolean;
|
|
16
16
|
extra_sessions: number;
|
|
17
|
+
/** Claude session colour from `/color` or auto-assignment (e.g. "blue"), or
|
|
18
|
+
* null when the session carries none. Drives the terminal tab tint. */
|
|
19
|
+
color: string | null;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export interface ISessionsListResponse {
|
package/src/widget.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { ServerConnection } from '@jupyterlab/services';
|
|
10
10
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
11
11
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
12
|
+
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
12
13
|
import { copyIcon, folderIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
13
14
|
import { CommandRegistry } from '@lumino/commands';
|
|
14
15
|
import { UUID } from '@lumino/coreutils';
|
|
@@ -54,6 +55,23 @@ export type PresentationMode = 'name' | 'path';
|
|
|
54
55
|
|
|
55
56
|
const DEFAULT_PRESENTATION_MODE: PresentationMode = 'name';
|
|
56
57
|
|
|
58
|
+
// Claude's per-session colour (set by `/color`, or auto-assigned for
|
|
59
|
+
// multi-session use) maps to a jupyterlab_colourful_tab_extension colour id.
|
|
60
|
+
// That extension owns the tab-tint CSS and the setColour API; we only feed it
|
|
61
|
+
// the colour. An absent or unrecognised colour resolves to null (clear tint).
|
|
62
|
+
const CLAUDE_TAB_COLOUR_ID: Record<string, string> = {
|
|
63
|
+
red: 'rose',
|
|
64
|
+
orange: 'peach',
|
|
65
|
+
yellow: 'lemon',
|
|
66
|
+
green: 'mint',
|
|
67
|
+
blue: 'sky',
|
|
68
|
+
purple: 'lavender'
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function claudeTabColourId(color: string | null | undefined): string | null {
|
|
72
|
+
return (color && CLAUDE_TAB_COLOUR_ID[color]) || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
const SECTION_LABELS: Record<SectionKey, string> = {
|
|
58
76
|
favourites: 'Favorites',
|
|
59
77
|
recent: 'Recent',
|
|
@@ -119,7 +137,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
119
137
|
app: JupyterFrontEnd,
|
|
120
138
|
rootDir: string,
|
|
121
139
|
terminalTracker: ITerminalTracker | null = null,
|
|
122
|
-
fileBrowser: IDefaultFileBrowser | null = null
|
|
140
|
+
fileBrowser: IDefaultFileBrowser | null = null,
|
|
141
|
+
colourfulTabs: IColourfulTabs | null = null
|
|
123
142
|
) {
|
|
124
143
|
super();
|
|
125
144
|
this._app = app;
|
|
@@ -127,6 +146,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
127
146
|
this._rootDir = rootDir.replace(/\/+$/, '');
|
|
128
147
|
this._terminalTracker = terminalTracker;
|
|
129
148
|
this._fileBrowser = fileBrowser;
|
|
149
|
+
this._colourfulTabs = colourfulTabs;
|
|
130
150
|
|
|
131
151
|
this.id = 'jupyterlab-claude-code-extension';
|
|
132
152
|
this.title.icon = claudeIcon;
|
|
@@ -408,6 +428,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
408
428
|
);
|
|
409
429
|
this._sessions = data.sessions ?? [];
|
|
410
430
|
this._render();
|
|
431
|
+
// Best-effort tab tinting; never let a colour failure break the fetch.
|
|
432
|
+
this._reconcileTerminalColours().catch(() => undefined);
|
|
411
433
|
}
|
|
412
434
|
|
|
413
435
|
private async _toggleFavourite(session: ISession): Promise<void> {
|
|
@@ -762,6 +784,123 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
762
784
|
});
|
|
763
785
|
}
|
|
764
786
|
|
|
787
|
+
/** Tint a terminal's dock tab with the colour of the Claude session it runs,
|
|
788
|
+
* delegating to jupyterlab_colourful_tab_extension's `setColour` API so that
|
|
789
|
+
* extension owns the tab CSS and colour vocabulary. A no-op when the
|
|
790
|
+
* colourful-tab extension is not installed (the token is optional). An
|
|
791
|
+
* absent/unknown colour clears the tint. */
|
|
792
|
+
private _applyTerminalColour(
|
|
793
|
+
widget: any,
|
|
794
|
+
color: string | null | undefined
|
|
795
|
+
): void {
|
|
796
|
+
if (!this._colourfulTabs || !widget || widget.isDisposed) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
this._colourfulTabs.setColour(widget, claudeTabColourId(color));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** Re-tint EVERY open Claude terminal tab from the freshest session colours,
|
|
803
|
+
* whether the plugin launched it or the user opened it themselves. Walks
|
|
804
|
+
* JupyterLab's terminal tracker (the registry of all open terminals) rather
|
|
805
|
+
* than only the plugin's own launch cache, and re-resolves each terminal's
|
|
806
|
+
* Claude conversation on every pass via the ``terminal-cwd`` probe - so a tab
|
|
807
|
+
* whose terminal has since started (or switched) a Claude conversation is
|
|
808
|
+
* re-tinted correctly rather than pinned to a stale identity. The probe is a
|
|
809
|
+
* few /proc reads per terminal; runs after each fetch (launch refresh + the
|
|
810
|
+
* 30s poll), which is already gated to skip while a context menu is open. */
|
|
811
|
+
private async _reconcileTerminalColours(): Promise<void> {
|
|
812
|
+
if (!this._colourfulTabs || !this._terminalTracker) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const sessions = this._sessions ?? [];
|
|
816
|
+
const widgets: any[] = [];
|
|
817
|
+
this._terminalTracker.forEach((widget: any) => {
|
|
818
|
+
if (widget && !widget.isDisposed) {
|
|
819
|
+
widgets.push(widget);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
await Promise.all(
|
|
823
|
+
widgets.map(async widget => {
|
|
824
|
+
const info = await this._interrogateTerminal(widget);
|
|
825
|
+
if (!info || !info.hasClaude || widget.isDisposed) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
this._applyTerminalColour(
|
|
829
|
+
widget,
|
|
830
|
+
this._colourForTerminal(info, sessions)
|
|
831
|
+
);
|
|
832
|
+
})
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Resolve a terminal's Claude identity: does it run Claude, which
|
|
837
|
+
* conversation id, and its cwd(s). Panel-launched terminals are already known
|
|
838
|
+
* (their conversation was confirmed at launch), so those skip the backend
|
|
839
|
+
* probe; every other terminal is probed once via ``terminal-cwd``. Returns
|
|
840
|
+
* ``null`` when the terminal has no session name yet or the probe fails. */
|
|
841
|
+
private async _interrogateTerminal(widget: any): Promise<{
|
|
842
|
+
hasClaude: boolean;
|
|
843
|
+
sessionId: string | null;
|
|
844
|
+
cwds: string[];
|
|
845
|
+
} | null> {
|
|
846
|
+
for (const entry of this._terminalsByPath.values()) {
|
|
847
|
+
if (entry.widget === widget && entry.sessionId) {
|
|
848
|
+
return { hasClaude: true, sessionId: entry.sessionId, cwds: [] };
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const sessName: string | undefined = widget?.content?.session?.name;
|
|
852
|
+
if (typeof sessName !== 'string' || !sessName) {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
const data = await requestAPI<ITerminalCwdResponse>(
|
|
857
|
+
`terminal-cwd/${encodeURIComponent(sessName)}`,
|
|
858
|
+
this._serverSettings
|
|
859
|
+
);
|
|
860
|
+
return {
|
|
861
|
+
hasClaude: !!data?.has_claude,
|
|
862
|
+
sessionId: data?.session_id ?? null,
|
|
863
|
+
cwds: Array.isArray(data?.cwds) ? data.cwds : []
|
|
864
|
+
};
|
|
865
|
+
} catch (_err) {
|
|
866
|
+
// Backend may 404 for a terminal that vanished between enumeration and
|
|
867
|
+
// probe - treat as unresolved and retry on the next poll.
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** Pick a terminal's tab colour from the session list. When the terminal runs
|
|
873
|
+
* a known conversation (its argv carried a ``--resume`` / ``--session-id``),
|
|
874
|
+
* match on that id ONLY: it takes the colour of the exact conversation, or -
|
|
875
|
+
* when that conversation is not the project's representative row - clears,
|
|
876
|
+
* rather than borrowing a sibling branch's colour. The cwd fallback is used
|
|
877
|
+
* ONLY for a terminal with no conversation id (a bare ``claude`` / ``-c``),
|
|
878
|
+
* matching the project the terminal is cwd'd in and preferring the longest
|
|
879
|
+
* matching path so a nested project wins over its parent. Returns null (clear)
|
|
880
|
+
* when no row matches or the matched row has no colour. */
|
|
881
|
+
private _colourForTerminal(
|
|
882
|
+
info: { sessionId: string | null; cwds: string[] },
|
|
883
|
+
sessions: ISession[]
|
|
884
|
+
): string | null {
|
|
885
|
+
if (info.sessionId) {
|
|
886
|
+
const bySession = sessions.find(s => s.session_id === info.sessionId);
|
|
887
|
+
return bySession ? (bySession.color ?? null) : null;
|
|
888
|
+
}
|
|
889
|
+
let best: ISession | undefined;
|
|
890
|
+
let bestLen = -1;
|
|
891
|
+
for (const raw of info.cwds) {
|
|
892
|
+
const cwd = raw.replace(/\/+$/, '');
|
|
893
|
+
for (const s of sessions) {
|
|
894
|
+
const p = s.project_path.replace(/\/+$/, '');
|
|
895
|
+
if ((cwd === p || cwd.startsWith(p + '/')) && p.length > bestLen) {
|
|
896
|
+
best = s;
|
|
897
|
+
bestLen = p.length;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return best ? (best.color ?? null) : null;
|
|
902
|
+
}
|
|
903
|
+
|
|
765
904
|
private async _findTerminalForCwd(
|
|
766
905
|
projectPath: string,
|
|
767
906
|
wantedSessionId: string | undefined
|
|
@@ -2205,6 +2344,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
2205
2344
|
private readonly _removingPaths: Set<string> = new Set();
|
|
2206
2345
|
private readonly _terminalTracker: ITerminalTracker | null;
|
|
2207
2346
|
private readonly _fileBrowser: IDefaultFileBrowser | null;
|
|
2347
|
+
private readonly _colourfulTabs: IColourfulTabs | null;
|
|
2208
2348
|
// Microcache of the most-recent terminal per project, tagged with the
|
|
2209
2349
|
// conversation it is running so reuse can tell a project's branches apart.
|
|
2210
2350
|
private readonly _terminalsByPath: Map<
|