jupyterlab_claude_code_extension 1.2.44 → 1.2.48
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 +13 -1
- package/lib/widget.js +24 -33
- package/package.json +1 -1
- package/src/__tests__/jupyterlab_claude_code_extension.spec.ts +24 -11
- package/src/widget.ts +24 -37
package/lib/widget.d.ts
CHANGED
|
@@ -114,7 +114,19 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
114
114
|
* matching path so a nested project wins over its parent. Returns null (clear)
|
|
115
115
|
* when no row matches or the matched row has no colour. */
|
|
116
116
|
private _colourForTerminal;
|
|
117
|
-
|
|
117
|
+
/** Find an open terminal running the wanted conversation, or null.
|
|
118
|
+
*
|
|
119
|
+
* Matches on the conversation id ALONE. The backend reads each terminal's
|
|
120
|
+
* true session id from the running claude (`~/.claude/sessions/<pid>.json`),
|
|
121
|
+
* so a bare `claude` / `-c` the user opened is identified too, not only
|
|
122
|
+
* extension launches that carry an argv id. A session id is globally unique
|
|
123
|
+
* to one conversation, so a terminal running it IS the one to focus -
|
|
124
|
+
* regardless of its reported cwd (a claude that cd'd into a subdir, or whose
|
|
125
|
+
* project dir was recreated, must still be reused, not duplicated). Reuse
|
|
126
|
+
* stays strict on identity: a terminal whose running session cannot be read
|
|
127
|
+
* (no claude, or an unreadable id) reports null, which never equals a wanted
|
|
128
|
+
* id, so a DIFFERENT conversation is never focused by mistake (DEF-4). */
|
|
129
|
+
private _findTerminalForSession;
|
|
118
130
|
private _showCloseExistingDialog;
|
|
119
131
|
/** Show a modal with a spinner while the terminal is being launched. The
|
|
120
132
|
* caller must dismiss it via ``.dispose()`` once the work is done - the
|
package/lib/widget.js
CHANGED
|
@@ -520,11 +520,11 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
520
520
|
return;
|
|
521
521
|
}
|
|
522
522
|
// 2. Walk every live terminal widget JL knows about.
|
|
523
|
-
const found = await this.
|
|
523
|
+
const found = await this._findTerminalForSession(session.session_id);
|
|
524
524
|
if (found) {
|
|
525
525
|
// Tag the cache with the OBSERVED conversation - here it equals the
|
|
526
|
-
// wanted id (the gate in
|
|
527
|
-
// a confirmed conversation rather than a wish.
|
|
526
|
+
// wanted id (the gate in _findTerminalForSession), so a later reuse
|
|
527
|
+
// trusts a confirmed conversation rather than a wish.
|
|
528
528
|
this._terminalsByPath.set(session.project_path, {
|
|
529
529
|
widget: found.widget,
|
|
530
530
|
sessionId: (_a = found.runningId) !== null && _a !== void 0 ? _a : undefined
|
|
@@ -758,9 +758,21 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
758
758
|
}
|
|
759
759
|
return best ? ((_b = best.color) !== null && _b !== void 0 ? _b : null) : null;
|
|
760
760
|
}
|
|
761
|
-
|
|
761
|
+
/** Find an open terminal running the wanted conversation, or null.
|
|
762
|
+
*
|
|
763
|
+
* Matches on the conversation id ALONE. The backend reads each terminal's
|
|
764
|
+
* true session id from the running claude (`~/.claude/sessions/<pid>.json`),
|
|
765
|
+
* so a bare `claude` / `-c` the user opened is identified too, not only
|
|
766
|
+
* extension launches that carry an argv id. A session id is globally unique
|
|
767
|
+
* to one conversation, so a terminal running it IS the one to focus -
|
|
768
|
+
* regardless of its reported cwd (a claude that cd'd into a subdir, or whose
|
|
769
|
+
* project dir was recreated, must still be reused, not duplicated). Reuse
|
|
770
|
+
* stays strict on identity: a terminal whose running session cannot be read
|
|
771
|
+
* (no claude, or an unreadable id) reports null, which never equals a wanted
|
|
772
|
+
* id, so a DIFFERENT conversation is never focused by mistake (DEF-4). */
|
|
773
|
+
async _findTerminalForSession(wantedSessionId) {
|
|
762
774
|
var _a, _b, _c;
|
|
763
|
-
if (!this._terminalTracker) {
|
|
775
|
+
if (!this._terminalTracker || !wantedSessionId) {
|
|
764
776
|
return null;
|
|
765
777
|
}
|
|
766
778
|
const candidates = [];
|
|
@@ -769,7 +781,6 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
769
781
|
candidates.push(widget);
|
|
770
782
|
}
|
|
771
783
|
});
|
|
772
|
-
const target = projectPath.replace(/\/+$/, '');
|
|
773
784
|
for (const widget of candidates) {
|
|
774
785
|
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;
|
|
775
786
|
if (typeof sessName !== 'string' || !sessName) {
|
|
@@ -777,33 +788,13 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
777
788
|
}
|
|
778
789
|
try {
|
|
779
790
|
const data = await requestAPI(`terminal-cwd/${encodeURIComponent(sessName)}`, this._serverSettings);
|
|
780
|
-
//
|
|
781
|
-
// a
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
// is running the wanted conversation. A terminal whose conversation
|
|
788
|
-
// is unknown (claude started with -c/--continue or a bare claude, so
|
|
789
|
-
// its argv carries no id) is never reused - it may be running a
|
|
790
|
-
// different conversation of this project (the switch-then-click bug).
|
|
791
|
-
// Extension launches always carry an explicit id, so an unknown
|
|
792
|
-
// terminal is necessarily foreign.
|
|
793
|
-
const runningId = (_c = data.session_id) !== null && _c !== void 0 ? _c : null;
|
|
794
|
-
// A missing wanted id (undefined) or an unknown running id (null) can
|
|
795
|
-
// never be a positive match - guard so neither side's "absent" value
|
|
796
|
-
// is mistaken for equality and an unknown terminal slips through.
|
|
797
|
-
if (!wantedSessionId || runningId !== wantedSessionId) {
|
|
798
|
-
continue;
|
|
799
|
-
}
|
|
800
|
-
const cwds = Array.isArray(data === null || data === void 0 ? void 0 : data.cwds) ? data.cwds : [];
|
|
801
|
-
for (const cwd of cwds) {
|
|
802
|
-
if ((cwd || '').replace(/\/+$/, '') === target) {
|
|
803
|
-
// runningId equals the wanted id here (gated above), so the caller
|
|
804
|
-
// tags its cache with a confirmed conversation id.
|
|
805
|
-
return { widget, runningId };
|
|
806
|
-
}
|
|
791
|
+
// Positive identity match on the running conversation. The backend
|
|
792
|
+
// reports a session id only for a live claude, so a match implies
|
|
793
|
+
// has_claude - the id alone is the gate, and the terminal's cwd is
|
|
794
|
+
// irrelevant once its conversation is positively known.
|
|
795
|
+
const runningId = (_c = data === null || data === void 0 ? void 0 : data.session_id) !== null && _c !== void 0 ? _c : null;
|
|
796
|
+
if (runningId === wantedSessionId) {
|
|
797
|
+
return { widget, runningId };
|
|
807
798
|
}
|
|
808
799
|
}
|
|
809
800
|
catch (_err) {
|
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.48",
|
|
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",
|
|
@@ -522,13 +522,15 @@ describe('launch spinner dismiss contract', () => {
|
|
|
522
522
|
* Contract for conversation-aware terminal reuse and the Open Branched
|
|
523
523
|
* Conversation feature. Reuse must refuse to focus a terminal running a
|
|
524
524
|
* DIFFERENT or UNKNOWN conversation (the switch-then-click bug, DEF-4): a
|
|
525
|
-
* terminal is reused only on a POSITIVE conversation-id match.
|
|
526
|
-
*
|
|
527
|
-
*
|
|
525
|
+
* terminal is reused only on a POSITIVE conversation-id match. The backend
|
|
526
|
+
* reads each terminal's true session id from the running claude's own
|
|
527
|
+
* `~/.claude/sessions/<pid>.json`, so a terminal the user opened with a bare
|
|
528
|
+
* `claude` / `-c` is identifiable too - reuse is not limited to extension
|
|
529
|
+
* launches that carry an id in argv.
|
|
528
530
|
*/
|
|
529
531
|
describe('conversation-aware reuse + open-branch contract', () => {
|
|
530
532
|
const findTerm = (widgetSrc.match(
|
|
531
|
-
/private async
|
|
533
|
+
/private async _findTerminalForSession[\s\S]*?\n \}/
|
|
532
534
|
) ?? [''])[0];
|
|
533
535
|
const doResume = (widgetSrc.match(
|
|
534
536
|
/private async _doResumeInTerminal[\s\S]*?\n \}/
|
|
@@ -553,19 +555,30 @@ describe('launch spinner dismiss contract', () => {
|
|
|
553
555
|
expect(iface).toMatch(/session_id\?: string \| null/);
|
|
554
556
|
});
|
|
555
557
|
|
|
556
|
-
it('
|
|
558
|
+
it('_findTerminalForSession keys on the session id alone (no cwd param)', () => {
|
|
557
559
|
expect(findTerm).toMatch(
|
|
558
|
-
/
|
|
560
|
+
/_findTerminalForSession\(\s*wantedSessionId: string \| undefined\s*\)/
|
|
559
561
|
);
|
|
562
|
+
// A missing wanted id short-circuits so null never matches null.
|
|
563
|
+
expect(findTerm).toMatch(
|
|
564
|
+
/if \(!this\._terminalTracker \|\| !wantedSessionId\)/
|
|
565
|
+
);
|
|
566
|
+
// Reuse is identity-only now - it must NOT gate on the terminal's cwd
|
|
567
|
+
// (a claude cd'd into a subdir, or a recreated project dir, must still
|
|
568
|
+
// be reused once its conversation is positively known).
|
|
569
|
+
expect(findTerm).not.toMatch(/cwds/);
|
|
570
|
+
expect(findTerm).not.toMatch(/=== target/);
|
|
560
571
|
});
|
|
561
572
|
|
|
562
573
|
it('reuse requires a positive conversation-id match (no lenient unknown reuse)', () => {
|
|
563
|
-
expect(findTerm).toMatch(/const runningId = data\.session_id \?\? null/);
|
|
564
|
-
// The ONLY reuse condition: a truthy wanted id equal to the observed
|
|
565
|
-
// id. A missing wanted id or an unknown (null) running id never matches,
|
|
566
|
-
// so a -c/--continue or bare-claude terminal is never reused (DEF-4).
|
|
567
574
|
expect(findTerm).toMatch(
|
|
568
|
-
/
|
|
575
|
+
/const runningId = data\?\.session_id \?\? null/
|
|
576
|
+
);
|
|
577
|
+
// The ONLY reuse condition: the observed running id equals the wanted
|
|
578
|
+
// id. An unknown (null) running id never equals a truthy wanted id, so a
|
|
579
|
+
// terminal whose conversation cannot be read is never reused (DEF-4).
|
|
580
|
+
expect(findTerm).toMatch(
|
|
581
|
+
/if \(runningId === wantedSessionId\) \{\s*return \{ widget, runningId \};/
|
|
569
582
|
);
|
|
570
583
|
// The old lenient/strict machinery is gone.
|
|
571
584
|
expect(findTerm).not.toMatch(/unknownConversation/);
|
package/src/widget.ts
CHANGED
|
@@ -635,14 +635,11 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
635
635
|
}
|
|
636
636
|
|
|
637
637
|
// 2. Walk every live terminal widget JL knows about.
|
|
638
|
-
const found = await this.
|
|
639
|
-
session.project_path,
|
|
640
|
-
session.session_id
|
|
641
|
-
);
|
|
638
|
+
const found = await this._findTerminalForSession(session.session_id);
|
|
642
639
|
if (found) {
|
|
643
640
|
// Tag the cache with the OBSERVED conversation - here it equals the
|
|
644
|
-
// wanted id (the gate in
|
|
645
|
-
// a confirmed conversation rather than a wish.
|
|
641
|
+
// wanted id (the gate in _findTerminalForSession), so a later reuse
|
|
642
|
+
// trusts a confirmed conversation rather than a wish.
|
|
646
643
|
this._terminalsByPath.set(session.project_path, {
|
|
647
644
|
widget: found.widget,
|
|
648
645
|
sessionId: found.runningId ?? undefined
|
|
@@ -901,11 +898,22 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
901
898
|
return best ? (best.color ?? null) : null;
|
|
902
899
|
}
|
|
903
900
|
|
|
904
|
-
|
|
905
|
-
|
|
901
|
+
/** Find an open terminal running the wanted conversation, or null.
|
|
902
|
+
*
|
|
903
|
+
* Matches on the conversation id ALONE. The backend reads each terminal's
|
|
904
|
+
* true session id from the running claude (`~/.claude/sessions/<pid>.json`),
|
|
905
|
+
* so a bare `claude` / `-c` the user opened is identified too, not only
|
|
906
|
+
* extension launches that carry an argv id. A session id is globally unique
|
|
907
|
+
* to one conversation, so a terminal running it IS the one to focus -
|
|
908
|
+
* regardless of its reported cwd (a claude that cd'd into a subdir, or whose
|
|
909
|
+
* project dir was recreated, must still be reused, not duplicated). Reuse
|
|
910
|
+
* stays strict on identity: a terminal whose running session cannot be read
|
|
911
|
+
* (no claude, or an unreadable id) reports null, which never equals a wanted
|
|
912
|
+
* id, so a DIFFERENT conversation is never focused by mistake (DEF-4). */
|
|
913
|
+
private async _findTerminalForSession(
|
|
906
914
|
wantedSessionId: string | undefined
|
|
907
915
|
): Promise<{ widget: any; runningId: string | null } | null> {
|
|
908
|
-
if (!this._terminalTracker) {
|
|
916
|
+
if (!this._terminalTracker || !wantedSessionId) {
|
|
909
917
|
return null;
|
|
910
918
|
}
|
|
911
919
|
const candidates: any[] = [];
|
|
@@ -914,7 +922,6 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
914
922
|
candidates.push(widget);
|
|
915
923
|
}
|
|
916
924
|
});
|
|
917
|
-
const target = projectPath.replace(/\/+$/, '');
|
|
918
925
|
for (const widget of candidates) {
|
|
919
926
|
const sessName: string | undefined = widget?.content?.session?.name;
|
|
920
927
|
if (typeof sessName !== 'string' || !sessName) {
|
|
@@ -925,33 +932,13 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
925
932
|
`terminal-cwd/${encodeURIComponent(sessName)}`,
|
|
926
933
|
this._serverSettings
|
|
927
934
|
);
|
|
928
|
-
//
|
|
929
|
-
// a
|
|
930
|
-
//
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
// is running the wanted conversation. A terminal whose conversation
|
|
936
|
-
// is unknown (claude started with -c/--continue or a bare claude, so
|
|
937
|
-
// its argv carries no id) is never reused - it may be running a
|
|
938
|
-
// different conversation of this project (the switch-then-click bug).
|
|
939
|
-
// Extension launches always carry an explicit id, so an unknown
|
|
940
|
-
// terminal is necessarily foreign.
|
|
941
|
-
const runningId = data.session_id ?? null;
|
|
942
|
-
// A missing wanted id (undefined) or an unknown running id (null) can
|
|
943
|
-
// never be a positive match - guard so neither side's "absent" value
|
|
944
|
-
// is mistaken for equality and an unknown terminal slips through.
|
|
945
|
-
if (!wantedSessionId || runningId !== wantedSessionId) {
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
const cwds = Array.isArray(data?.cwds) ? data.cwds : [];
|
|
949
|
-
for (const cwd of cwds) {
|
|
950
|
-
if ((cwd || '').replace(/\/+$/, '') === target) {
|
|
951
|
-
// runningId equals the wanted id here (gated above), so the caller
|
|
952
|
-
// tags its cache with a confirmed conversation id.
|
|
953
|
-
return { widget, runningId };
|
|
954
|
-
}
|
|
935
|
+
// Positive identity match on the running conversation. The backend
|
|
936
|
+
// reports a session id only for a live claude, so a match implies
|
|
937
|
+
// has_claude - the id alone is the gate, and the terminal's cwd is
|
|
938
|
+
// irrelevant once its conversation is positively known.
|
|
939
|
+
const runningId = data?.session_id ?? null;
|
|
940
|
+
if (runningId === wantedSessionId) {
|
|
941
|
+
return { widget, runningId };
|
|
955
942
|
}
|
|
956
943
|
} catch (_err) {
|
|
957
944
|
// Backend may report 404 for terminals that disappeared between
|