jupyterlab_claude_code_extension 1.2.42 → 1.2.44

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;
@@ -767,9 +838,11 @@ export class ClaudeCodeSessionsWidget extends Widget {
767
838
  body,
768
839
  buttons: []
769
840
  });
770
- // launch() returns a Promise we don't await - we resolve programmatically
771
- // when the spawn completes (or errors).
772
- void dialog.launch();
841
+ // launch() returns a Promise we don't await - the caller dismisses this
842
+ // dialog with .dispose() when the spawn completes. Disposing an open Lumino
843
+ // dialog rejects that promise with `undefined`, so catch it here to keep a
844
+ // benign teardown from surfacing as an unhandled promise rejection.
845
+ dialog.launch().catch(() => undefined);
773
846
  return dialog;
774
847
  }
775
848
  _wireTerminalDisposal(projectPath, widget) {
@@ -1171,7 +1244,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1171
1244
  Notification.warning('Folder is outside the JupyterLab root - cannot open a terminal there.', { autoClose: 4000 });
1172
1245
  return;
1173
1246
  }
1174
- void this._app.commands.execute('terminal:create-new', { cwd: rel });
1247
+ this._app.commands
1248
+ .execute('terminal:create-new', { cwd: rel })
1249
+ .catch(err => this._showError(err));
1175
1250
  }
1176
1251
  });
1177
1252
  this._commands.addCommand('claude-code-sessions:show-in-filebrowser', {
@@ -1190,9 +1265,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1190
1265
  }
1191
1266
  // JL's built-in command navigates the default file browser to the
1192
1267
  // path and reveals the browser panel.
1193
- void this._app.commands.execute('filebrowser:go-to-path', {
1194
- path: rel
1195
- });
1268
+ this._app.commands
1269
+ .execute('filebrowser:go-to-path', { path: rel })
1270
+ .catch(err => this._showError(err));
1196
1271
  }
1197
1272
  });
1198
1273
  this._commands.addCommand('claude-code-sessions:copy-path', {
@@ -1760,7 +1835,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1760
1835
  });
1761
1836
  render();
1762
1837
  updateControls();
1763
- void dialog.launch();
1838
+ // dialog.dispose() (on open/switch) rejects this promise with `undefined`;
1839
+ // catch it so the teardown never surfaces as an unhandled promise rejection.
1840
+ dialog.launch().catch(() => undefined);
1764
1841
  search.focus();
1765
1842
  }
1766
1843
  /** Delete the given branch sessions of the active row's project.
@@ -1890,7 +1967,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1890
1967
  : `Branch switch failed: ${err}`, { autoClose: 4000 });
1891
1968
  }
1892
1969
  finally {
1893
- await this._fetch();
1970
+ // Best-effort refresh - never let a transient fetch failure reject this
1971
+ // fire-and-forget switch (its callers do not await it).
1972
+ await this._fetch().catch(() => undefined);
1894
1973
  }
1895
1974
  }
1896
1975
  /** Open a specific conversation branch in its own terminal.
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.44",
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(
@@ -906,9 +990,11 @@ export class ClaudeCodeSessionsWidget extends Widget {
906
990
  body,
907
991
  buttons: []
908
992
  });
909
- // launch() returns a Promise we don't await - we resolve programmatically
910
- // when the spawn completes (or errors).
911
- void dialog.launch();
993
+ // launch() returns a Promise we don't await - the caller dismisses this
994
+ // dialog with .dispose() when the spawn completes. Disposing an open Lumino
995
+ // dialog rejects that promise with `undefined`, so catch it here to keep a
996
+ // benign teardown from surfacing as an unhandled promise rejection.
997
+ dialog.launch().catch(() => undefined);
912
998
  return dialog;
913
999
  }
914
1000
 
@@ -1356,7 +1442,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1356
1442
  );
1357
1443
  return;
1358
1444
  }
1359
- void this._app.commands.execute('terminal:create-new', { cwd: rel });
1445
+ this._app.commands
1446
+ .execute('terminal:create-new', { cwd: rel })
1447
+ .catch(err => this._showError(err));
1360
1448
  }
1361
1449
  });
1362
1450
 
@@ -1379,9 +1467,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
1379
1467
  }
1380
1468
  // JL's built-in command navigates the default file browser to the
1381
1469
  // path and reveals the browser panel.
1382
- void this._app.commands.execute('filebrowser:go-to-path', {
1383
- path: rel
1384
- });
1470
+ this._app.commands
1471
+ .execute('filebrowser:go-to-path', { path: rel })
1472
+ .catch(err => this._showError(err));
1385
1473
  }
1386
1474
  });
1387
1475
 
@@ -2009,7 +2097,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
2009
2097
  render();
2010
2098
  updateControls();
2011
2099
 
2012
- void dialog.launch();
2100
+ // dialog.dispose() (on open/switch) rejects this promise with `undefined`;
2101
+ // catch it so the teardown never surfaces as an unhandled promise rejection.
2102
+ dialog.launch().catch(() => undefined);
2013
2103
  search.focus();
2014
2104
  }
2015
2105
 
@@ -2156,7 +2246,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
2156
2246
  { autoClose: 4000 }
2157
2247
  );
2158
2248
  } finally {
2159
- await this._fetch();
2249
+ // Best-effort refresh - never let a transient fetch failure reject this
2250
+ // fire-and-forget switch (its callers do not await it).
2251
+ await this._fetch().catch(() => undefined);
2160
2252
  }
2161
2253
  }
2162
2254