jupyterlab_claude_code_extension 1.2.42 → 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/lib/widget.d.ts +25 -6
- package/lib/widget.js +85 -14
- package/package.json +1 -1
- package/src/__tests__/jupyterlab_claude_code_extension.spec.ts +1 -0
- package/src/widget.ts +99 -15
package/lib/widget.d.ts
CHANGED
|
@@ -88,13 +88,32 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
88
88
|
* colourful-tab extension is not installed (the token is optional). An
|
|
89
89
|
* absent/unknown colour clears the tint. */
|
|
90
90
|
private _applyTerminalColour;
|
|
91
|
-
/** Re-tint
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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. */
|
|
97
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;
|
|
98
117
|
private _findTerminalForCwd;
|
|
99
118
|
private _showCloseExistingDialog;
|
|
100
119
|
/** Show a modal with a spinner while the terminal is being launched. The
|
package/lib/widget.js
CHANGED
|
@@ -353,7 +353,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
353
353
|
const data = await requestAPI('sessions', this._serverSettings, { cache: 'no-store' });
|
|
354
354
|
this._sessions = (_a = data.sessions) !== null && _a !== void 0 ? _a : [];
|
|
355
355
|
this._render();
|
|
356
|
-
|
|
356
|
+
// Best-effort tab tinting; never let a colour failure break the fetch.
|
|
357
|
+
this._reconcileTerminalColours().catch(() => undefined);
|
|
357
358
|
}
|
|
358
359
|
async _toggleFavourite(session) {
|
|
359
360
|
const next = !session.favourite;
|
|
@@ -669,23 +670,93 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
669
670
|
}
|
|
670
671
|
this._colourfulTabs.setColour(widget, claudeTabColourId(color));
|
|
671
672
|
}
|
|
672
|
-
/** Re-tint
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
|
|
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() {
|
|
679
683
|
var _a;
|
|
684
|
+
if (!this._colourfulTabs || !this._terminalTracker) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
680
687
|
const sessions = (_a = this._sessions) !== null && _a !== void 0 ? _a : [];
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
|
|
688
|
+
const widgets = [];
|
|
689
|
+
this._terminalTracker.forEach((widget) => {
|
|
690
|
+
if (widget && !widget.isDisposed) {
|
|
691
|
+
widgets.push(widget);
|
|
685
692
|
}
|
|
686
|
-
const match = sessions.find(s => s.project_path === projectPath && s.session_id === entry.sessionId);
|
|
687
|
-
this._applyTerminalColour(entry.widget, (_a = match === null || match === void 0 ? void 0 : match.color) !== null && _a !== void 0 ? _a : null);
|
|
688
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;
|
|
689
760
|
}
|
|
690
761
|
async _findTerminalForCwd(projectPath, wantedSessionId) {
|
|
691
762
|
var _a, _b, _c;
|
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",
|
package/src/widget.ts
CHANGED
|
@@ -428,7 +428,8 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
428
428
|
);
|
|
429
429
|
this._sessions = data.sessions ?? [];
|
|
430
430
|
this._render();
|
|
431
|
-
|
|
431
|
+
// Best-effort tab tinting; never let a colour failure break the fetch.
|
|
432
|
+
this._reconcileTerminalColours().catch(() => undefined);
|
|
432
433
|
}
|
|
433
434
|
|
|
434
435
|
private async _toggleFavourite(session: ISession): Promise<void> {
|
|
@@ -798,23 +799,106 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
798
799
|
this._colourfulTabs.setColour(widget, claudeTabColourId(color));
|
|
799
800
|
}
|
|
800
801
|
|
|
801
|
-
/** Re-tint
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
|
|
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
|
+
}
|
|
808
815
|
const sessions = this._sessions ?? [];
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
816
|
+
const widgets: any[] = [];
|
|
817
|
+
this._terminalTracker.forEach((widget: any) => {
|
|
818
|
+
if (widget && !widget.isDisposed) {
|
|
819
|
+
widgets.push(widget);
|
|
812
820
|
}
|
|
813
|
-
const match = sessions.find(
|
|
814
|
-
s => s.project_path === projectPath && s.session_id === entry.sessionId
|
|
815
|
-
);
|
|
816
|
-
this._applyTerminalColour(entry.widget, match?.color ?? null);
|
|
817
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;
|
|
818
902
|
}
|
|
819
903
|
|
|
820
904
|
private async _findTerminalForCwd(
|