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 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
- private _findTerminalForCwd;
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._findTerminalForCwd(session.project_path, session.session_id);
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 _findTerminalForCwd), so a later reuse trusts
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
- async _findTerminalForCwd(projectPath, wantedSessionId) {
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
- // Only reuse terminals that actually have claude running. Otherwise
781
- // a plain bash opened at the project cwd would be matched and
782
- // activated, swallowing the resume click without spawning claude.
783
- if (!(data === null || data === void 0 ? void 0 : data.has_claude)) {
784
- continue;
785
- }
786
- // Conversation gate: reuse ONLY a terminal we can positively confirm
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.44",
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. Every
526
- * extension launch carries an explicit id so its terminal is identifiable
527
- * (resume -> --resume, new session / fork -> --session-id).
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 _findTerminalForCwd[\s\S]*?\n \}/
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('_findTerminalForCwd takes the wanted id with no strict flag', () => {
558
+ it('_findTerminalForSession keys on the session id alone (no cwd param)', () => {
557
559
  expect(findTerm).toMatch(
558
- /_findTerminalForCwd\(\s*projectPath: string,\s*wantedSessionId: string \| undefined\s*\)/
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
- /if \(!wantedSessionId \|\| runningId !== wantedSessionId\) \{\s*continue;/
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._findTerminalForCwd(
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 _findTerminalForCwd), so a later reuse trusts
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
- private async _findTerminalForCwd(
905
- projectPath: string,
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
- // Only reuse terminals that actually have claude running. Otherwise
929
- // a plain bash opened at the project cwd would be matched and
930
- // activated, swallowing the resume click without spawning claude.
931
- if (!data?.has_claude) {
932
- continue;
933
- }
934
- // Conversation gate: reuse ONLY a terminal we can positively confirm
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