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 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 every tracked terminal tab from the freshest session colours.
92
- * Runs after each fetch (launch refresh + the 30s poll), so a `/color`
93
- * change shows on the next tick. Matches on project AND conversation so a
94
- * tab only takes the colour of the exact session it runs; a terminal
95
- * running a branch other than the project's representative row stays
96
- * untinted rather than borrowing a sibling's colour. */
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
- this._reconcileTerminalColours();
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 every tracked terminal tab from the freshest session colours.
673
- * Runs after each fetch (launch refresh + the 30s poll), so a `/color`
674
- * change shows on the next tick. Matches on project AND conversation so a
675
- * tab only takes the colour of the exact session it runs; a terminal
676
- * running a branch other than the project's representative row stays
677
- * untinted rather than borrowing a sibling's colour. */
678
- _reconcileTerminalColours() {
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
- this._terminalsByPath.forEach((entry, projectPath) => {
682
- var _a;
683
- if (!entry.widget || entry.widget.isDisposed) {
684
- return;
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.42",
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",
@@ -23,6 +23,7 @@ const session = (over: Partial<ISession> = {}): ISession => ({
23
23
  remote_control: false,
24
24
  favourite: false,
25
25
  extra_sessions: 0,
26
+ color: null,
26
27
  ...over
27
28
  });
28
29
 
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
- this._reconcileTerminalColours();
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 every tracked terminal tab from the freshest session colours.
802
- * Runs after each fetch (launch refresh + the 30s poll), so a `/color`
803
- * change shows on the next tick. Matches on project AND conversation so a
804
- * tab only takes the colour of the exact session it runs; a terminal
805
- * running a branch other than the project's representative row stays
806
- * untinted rather than borrowing a sibling's colour. */
807
- private _reconcileTerminalColours(): void {
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
- this._terminalsByPath.forEach((entry, projectPath) => {
810
- if (!entry.widget || entry.widget.isDisposed) {
811
- return;
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(