jupyterlab_claude_code_extension 1.2.48 → 1.2.49
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/colour.d.ts +15 -0
- package/lib/colour.js +40 -0
- package/lib/widget.d.ts +4 -15
- package/lib/widget.js +10 -56
- package/package.json +1 -1
- package/src/__tests__/jupyterlab_claude_code_extension.spec.ts +89 -0
- package/src/colour.ts +60 -0
- package/src/widget.ts +14 -62
package/lib/colour.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ISession } from './types';
|
|
2
|
+
export declare function claudeTabColourId(color: string | null | undefined): string | null;
|
|
3
|
+
/** What a terminal is running, as resolved by the `terminal-cwd` probe. */
|
|
4
|
+
export interface ITerminalColourInfo {
|
|
5
|
+
sessionId: string | null;
|
|
6
|
+
color: string | null;
|
|
7
|
+
cwds: string[];
|
|
8
|
+
}
|
|
9
|
+
/** A terminal takes its OWN conversation's colour. Never resolve colour via
|
|
10
|
+
* the project row: a row carries only the representative conversation, which
|
|
11
|
+
* flips to any newer JSONL (a daemon `--fork-session` writes one), so
|
|
12
|
+
* row-matching cleared every non-representative terminal's tint (DEF-11). cwd
|
|
13
|
+
* fallback only when the conversation is unreadable - longest path wins, so a
|
|
14
|
+
* nested project beats its parent. Null clears. */
|
|
15
|
+
export declare function colourForTerminal(info: ITerminalColourInfo, sessions: ISession[]): string | null;
|
package/lib/colour.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Claude's per-session colour (`/color`, or auto-assigned) maps to a
|
|
2
|
+
// jupyterlab_colourful_tab_extension colour id. That extension owns the tab
|
|
3
|
+
// CSS and colour vocabulary; we only feed it the colour. Absent or
|
|
4
|
+
// unrecognised resolves to null (clear tint).
|
|
5
|
+
const CLAUDE_TAB_COLOUR_ID = {
|
|
6
|
+
red: 'rose',
|
|
7
|
+
orange: 'peach',
|
|
8
|
+
yellow: 'lemon',
|
|
9
|
+
green: 'mint',
|
|
10
|
+
blue: 'sky',
|
|
11
|
+
purple: 'lavender'
|
|
12
|
+
};
|
|
13
|
+
export function claudeTabColourId(color) {
|
|
14
|
+
return (color && CLAUDE_TAB_COLOUR_ID[color]) || null;
|
|
15
|
+
}
|
|
16
|
+
/** A terminal takes its OWN conversation's colour. Never resolve colour via
|
|
17
|
+
* the project row: a row carries only the representative conversation, which
|
|
18
|
+
* flips to any newer JSONL (a daemon `--fork-session` writes one), so
|
|
19
|
+
* row-matching cleared every non-representative terminal's tint (DEF-11). cwd
|
|
20
|
+
* fallback only when the conversation is unreadable - longest path wins, so a
|
|
21
|
+
* nested project beats its parent. Null clears. */
|
|
22
|
+
export function colourForTerminal(info, sessions) {
|
|
23
|
+
var _a, _b;
|
|
24
|
+
if (info.sessionId) {
|
|
25
|
+
return (_a = info.color) !== null && _a !== void 0 ? _a : null;
|
|
26
|
+
}
|
|
27
|
+
let best;
|
|
28
|
+
let bestLen = -1;
|
|
29
|
+
for (const raw of info.cwds) {
|
|
30
|
+
const cwd = raw.replace(/\/+$/, '');
|
|
31
|
+
for (const s of sessions) {
|
|
32
|
+
const p = s.project_path.replace(/\/+$/, '');
|
|
33
|
+
if ((cwd === p || cwd.startsWith(p + '/')) && p.length > bestLen) {
|
|
34
|
+
best = s;
|
|
35
|
+
bestLen = p.length;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return best ? ((_b = best.color) !== null && _b !== void 0 ? _b : null) : null;
|
|
40
|
+
}
|
package/lib/widget.d.ts
CHANGED
|
@@ -98,22 +98,11 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
98
98
|
* few /proc reads per terminal; runs after each fetch (launch refresh + the
|
|
99
99
|
* 30s poll), which is already gated to skip while a context menu is open. */
|
|
100
100
|
private _reconcileTerminalColours;
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* ``null`` when the terminal has no session name yet or the probe fails. */
|
|
101
|
+
/** Probe a terminal: runs Claude?, which conversation, its colour, its
|
|
102
|
+
* cwd(s). Probes EVERY terminal every pass - the launch cache is not
|
|
103
|
+
* consulted, it pins the launch-time conversation and goes stale on an
|
|
104
|
+
* in-place switch. Null when no session name yet, or the probe fails. */
|
|
106
105
|
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;
|
|
117
106
|
/** Find an open terminal running the wanted conversation, or null.
|
|
118
107
|
*
|
|
119
108
|
* Matches on the conversation id ALONE. The backend reads each terminal's
|
package/lib/widget.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CommandRegistry } from '@lumino/commands';
|
|
|
5
5
|
import { UUID } from '@lumino/coreutils';
|
|
6
6
|
import { Menu, Widget } from '@lumino/widgets';
|
|
7
7
|
import { requestAPI } from './request';
|
|
8
|
+
import { claudeTabColourId, colourForTerminal } from './colour';
|
|
8
9
|
import { addIcon, branchIcon, switchIcon, claudeIcon, filterIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon } from './icons';
|
|
9
10
|
const POLL_INTERVAL_MS = 30000;
|
|
10
11
|
// After a fork is requested, watch for its lazily-written JSONL at this fast
|
|
@@ -14,21 +15,8 @@ const BRANCH_WATCH_MAX_ATTEMPTS = 90; // ~3 minutes
|
|
|
14
15
|
const DEFAULT_RECENT_LIMIT = 10;
|
|
15
16
|
const EXPANDED_STORAGE_KEY = 'jupyterlab_claude_code_extension:expanded';
|
|
16
17
|
const DEFAULT_PRESENTATION_MODE = 'name';
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// That extension owns the tab-tint CSS and the setColour API; we only feed it
|
|
20
|
-
// the colour. An absent or unrecognised colour resolves to null (clear tint).
|
|
21
|
-
const CLAUDE_TAB_COLOUR_ID = {
|
|
22
|
-
red: 'rose',
|
|
23
|
-
orange: 'peach',
|
|
24
|
-
yellow: 'lemon',
|
|
25
|
-
green: 'mint',
|
|
26
|
-
blue: 'sky',
|
|
27
|
-
purple: 'lavender'
|
|
28
|
-
};
|
|
29
|
-
function claudeTabColourId(color) {
|
|
30
|
-
return (color && CLAUDE_TAB_COLOUR_ID[color]) || null;
|
|
31
|
-
}
|
|
18
|
+
// Colour resolution lives in ./colour - pure, JupyterLab-free, and therefore
|
|
19
|
+
// executable under jest (the tint regressed twice behind a green suite).
|
|
32
20
|
const SECTION_LABELS = {
|
|
33
21
|
favourites: 'Favorites',
|
|
34
22
|
recent: 'Recent',
|
|
@@ -696,21 +684,15 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
696
684
|
if (!info || !info.hasClaude || widget.isDisposed) {
|
|
697
685
|
return;
|
|
698
686
|
}
|
|
699
|
-
this._applyTerminalColour(widget,
|
|
687
|
+
this._applyTerminalColour(widget, colourForTerminal(info, sessions));
|
|
700
688
|
}));
|
|
701
689
|
}
|
|
702
|
-
/**
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
* ``null`` when the terminal has no session name yet or the probe fails. */
|
|
690
|
+
/** Probe a terminal: runs Claude?, which conversation, its colour, its
|
|
691
|
+
* cwd(s). Probes EVERY terminal every pass - the launch cache is not
|
|
692
|
+
* consulted, it pins the launch-time conversation and goes stale on an
|
|
693
|
+
* in-place switch. Null when no session name yet, or the probe fails. */
|
|
707
694
|
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
|
-
}
|
|
695
|
+
var _a, _b, _c, _d;
|
|
714
696
|
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
697
|
if (typeof sessName !== 'string' || !sessName) {
|
|
716
698
|
return null;
|
|
@@ -720,6 +702,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
720
702
|
return {
|
|
721
703
|
hasClaude: !!(data === null || data === void 0 ? void 0 : data.has_claude),
|
|
722
704
|
sessionId: (_c = data === null || data === void 0 ? void 0 : data.session_id) !== null && _c !== void 0 ? _c : null,
|
|
705
|
+
color: (_d = data === null || data === void 0 ? void 0 : data.color) !== null && _d !== void 0 ? _d : null,
|
|
723
706
|
cwds: Array.isArray(data === null || data === void 0 ? void 0 : data.cwds) ? data.cwds : []
|
|
724
707
|
};
|
|
725
708
|
}
|
|
@@ -729,35 +712,6 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
729
712
|
return null;
|
|
730
713
|
}
|
|
731
714
|
}
|
|
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;
|
|
760
|
-
}
|
|
761
715
|
/** Find an open terminal running the wanted conversation, or null.
|
|
762
716
|
*
|
|
763
717
|
* Matches on the conversation id ALONE. The backend reads each terminal's
|
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.49",
|
|
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",
|
|
@@ -6,6 +6,9 @@ const fs: { readFileSync: (p: string, enc: string) => string } = require('fs');
|
|
|
6
6
|
const path: { join: (...args: string[]) => string } = require('path');
|
|
7
7
|
|
|
8
8
|
import type { ISession } from '../types';
|
|
9
|
+
// Executed, not grepped - src/colour.ts is JupyterLab-free by design so the
|
|
10
|
+
// tint logic can actually be run under jest.
|
|
11
|
+
import { claudeTabColourId, colourForTerminal } from '../colour';
|
|
9
12
|
|
|
10
13
|
const session = (over: Partial<ISession> = {}): ISession => ({
|
|
11
14
|
project_path: '/p',
|
|
@@ -528,6 +531,92 @@ describe('launch spinner dismiss contract', () => {
|
|
|
528
531
|
* `claude` / `-c` is identifiable too - reuse is not limited to extension
|
|
529
532
|
* launches that carry an id in argv.
|
|
530
533
|
*/
|
|
534
|
+
/**
|
|
535
|
+
* Tab colour (DEF-11). A terminal's tint is the colour of the conversation
|
|
536
|
+
* it RUNS, which the backend reads from that conversation's own JSONL.
|
|
537
|
+
* Resolving the tint through the project row is the bug: a row carries only
|
|
538
|
+
* the project's representative conversation, and the representative flips to
|
|
539
|
+
* any newer JSONL - a daemon `--fork-session` worker writes one - which
|
|
540
|
+
* cleared the tint of every terminal not on the current representative.
|
|
541
|
+
*/
|
|
542
|
+
describe('terminal tab colour', () => {
|
|
543
|
+
// These EXECUTE the shipped code (src/colour.ts) rather than grepping its
|
|
544
|
+
// text: the tint regressed twice (DEF-10, DEF-11) behind a green suite
|
|
545
|
+
// that ran none of it.
|
|
546
|
+
const rows = [
|
|
547
|
+
session({ project_path: '/w', session_id: 'ws', color: 'blue' }),
|
|
548
|
+
session({ project_path: '/w/proj', session_id: 'rep', color: 'green' })
|
|
549
|
+
];
|
|
550
|
+
|
|
551
|
+
it('takes the running conversation colour, not the row (DEF-11)', () => {
|
|
552
|
+
// The row's representative is 'rep'/green - a daemon fork that stole the
|
|
553
|
+
// slot. The terminal runs 'mine', whose own colour is orange. Resolving
|
|
554
|
+
// via the rows found no match and CLEARED the tint; it must not.
|
|
555
|
+
expect(
|
|
556
|
+
colourForTerminal(
|
|
557
|
+
{ sessionId: 'mine', color: 'orange', cwds: ['/w/proj'] },
|
|
558
|
+
rows
|
|
559
|
+
)
|
|
560
|
+
).toBe('orange');
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('a conversation with no colour of its own clears', () => {
|
|
564
|
+
expect(
|
|
565
|
+
colourForTerminal(
|
|
566
|
+
{ sessionId: 'mine', color: null, cwds: ['/w'] },
|
|
567
|
+
rows
|
|
568
|
+
)
|
|
569
|
+
).toBeNull();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('falls back to cwd only when the conversation is unreadable', () => {
|
|
573
|
+
expect(
|
|
574
|
+
colourForTerminal(
|
|
575
|
+
{ sessionId: null, color: null, cwds: ['/w/proj'] },
|
|
576
|
+
rows
|
|
577
|
+
)
|
|
578
|
+
).toBe('green');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('cwd fallback prefers the nested project over its parent', () => {
|
|
582
|
+
expect(
|
|
583
|
+
colourForTerminal(
|
|
584
|
+
{ sessionId: null, color: null, cwds: ['/w/proj/deep'] },
|
|
585
|
+
rows
|
|
586
|
+
)
|
|
587
|
+
).toBe('green');
|
|
588
|
+
expect(
|
|
589
|
+
colourForTerminal(
|
|
590
|
+
{ sessionId: null, color: null, cwds: ['/w/other'] },
|
|
591
|
+
rows
|
|
592
|
+
)
|
|
593
|
+
).toBe('blue');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('cwd fallback does not match a sibling on a shared prefix', () => {
|
|
597
|
+
// '/w/proj-extra' must not match '/w/proj' - the boundary is a real '/'.
|
|
598
|
+
expect(
|
|
599
|
+
colourForTerminal(
|
|
600
|
+
{ sessionId: null, color: null, cwds: ['/w/proj-extra'] },
|
|
601
|
+
[
|
|
602
|
+
session({
|
|
603
|
+
project_path: '/w/proj',
|
|
604
|
+
session_id: 'r',
|
|
605
|
+
color: 'green'
|
|
606
|
+
})
|
|
607
|
+
]
|
|
608
|
+
)
|
|
609
|
+
).toBeNull();
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('maps Claude colours to tab ids and drops unknown ones', () => {
|
|
613
|
+
expect(claudeTabColourId('orange')).toBe('peach');
|
|
614
|
+
expect(claudeTabColourId('green')).toBe('mint');
|
|
615
|
+
expect(claudeTabColourId('chartreuse')).toBeNull();
|
|
616
|
+
expect(claudeTabColourId(null)).toBeNull();
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
531
620
|
describe('conversation-aware reuse + open-branch contract', () => {
|
|
532
621
|
const findTerm = (widgetSrc.match(
|
|
533
622
|
/private async _findTerminalForSession[\s\S]*?\n \}/
|
package/src/colour.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Pure colour resolution for Claude terminal tabs. Kept free of JupyterLab
|
|
2
|
+
// imports so it is directly executable under jest - the tint regressed twice
|
|
3
|
+
// (DEF-10, DEF-11) behind a green suite that never ran this logic.
|
|
4
|
+
import type { ISession } from './types';
|
|
5
|
+
|
|
6
|
+
// Claude's per-session colour (`/color`, or auto-assigned) maps to a
|
|
7
|
+
// jupyterlab_colourful_tab_extension colour id. That extension owns the tab
|
|
8
|
+
// CSS and colour vocabulary; we only feed it the colour. Absent or
|
|
9
|
+
// unrecognised resolves to null (clear tint).
|
|
10
|
+
const CLAUDE_TAB_COLOUR_ID: Record<string, string> = {
|
|
11
|
+
red: 'rose',
|
|
12
|
+
orange: 'peach',
|
|
13
|
+
yellow: 'lemon',
|
|
14
|
+
green: 'mint',
|
|
15
|
+
blue: 'sky',
|
|
16
|
+
purple: 'lavender'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function claudeTabColourId(
|
|
20
|
+
color: string | null | undefined
|
|
21
|
+
): string | null {
|
|
22
|
+
return (color && CLAUDE_TAB_COLOUR_ID[color]) || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What a terminal is running, as resolved by the `terminal-cwd` probe. */
|
|
26
|
+
export interface ITerminalColourInfo {
|
|
27
|
+
// Conversation the terminal runs, or null when it cannot be read.
|
|
28
|
+
sessionId: string | null;
|
|
29
|
+
// That conversation's OWN colour, read from its own JSONL by the backend.
|
|
30
|
+
color: string | null;
|
|
31
|
+
cwds: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A terminal takes its OWN conversation's colour. Never resolve colour via
|
|
35
|
+
* the project row: a row carries only the representative conversation, which
|
|
36
|
+
* flips to any newer JSONL (a daemon `--fork-session` writes one), so
|
|
37
|
+
* row-matching cleared every non-representative terminal's tint (DEF-11). cwd
|
|
38
|
+
* fallback only when the conversation is unreadable - longest path wins, so a
|
|
39
|
+
* nested project beats its parent. Null clears. */
|
|
40
|
+
export function colourForTerminal(
|
|
41
|
+
info: ITerminalColourInfo,
|
|
42
|
+
sessions: ISession[]
|
|
43
|
+
): string | null {
|
|
44
|
+
if (info.sessionId) {
|
|
45
|
+
return info.color ?? null;
|
|
46
|
+
}
|
|
47
|
+
let best: ISession | undefined;
|
|
48
|
+
let bestLen = -1;
|
|
49
|
+
for (const raw of info.cwds) {
|
|
50
|
+
const cwd = raw.replace(/\/+$/, '');
|
|
51
|
+
for (const s of sessions) {
|
|
52
|
+
const p = s.project_path.replace(/\/+$/, '');
|
|
53
|
+
if ((cwd === p || cwd.startsWith(p + '/')) && p.length > bestLen) {
|
|
54
|
+
best = s;
|
|
55
|
+
bestLen = p.length;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return best ? (best.color ?? null) : null;
|
|
60
|
+
}
|
package/src/widget.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { Menu, Widget } from '@lumino/widgets';
|
|
|
17
17
|
import { Message } from '@lumino/messaging';
|
|
18
18
|
|
|
19
19
|
import { requestAPI } from './request';
|
|
20
|
+
import { claudeTabColourId, colourForTerminal } from './colour';
|
|
20
21
|
import {
|
|
21
22
|
addIcon,
|
|
22
23
|
branchIcon,
|
|
@@ -55,22 +56,8 @@ export type PresentationMode = 'name' | 'path';
|
|
|
55
56
|
|
|
56
57
|
const DEFAULT_PRESENTATION_MODE: PresentationMode = 'name';
|
|
57
58
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// That extension owns the tab-tint CSS and the setColour API; we only feed it
|
|
61
|
-
// the colour. An absent or unrecognised colour resolves to null (clear tint).
|
|
62
|
-
const CLAUDE_TAB_COLOUR_ID: Record<string, string> = {
|
|
63
|
-
red: 'rose',
|
|
64
|
-
orange: 'peach',
|
|
65
|
-
yellow: 'lemon',
|
|
66
|
-
green: 'mint',
|
|
67
|
-
blue: 'sky',
|
|
68
|
-
purple: 'lavender'
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
function claudeTabColourId(color: string | null | undefined): string | null {
|
|
72
|
-
return (color && CLAUDE_TAB_COLOUR_ID[color]) || null;
|
|
73
|
-
}
|
|
59
|
+
// Colour resolution lives in ./colour - pure, JupyterLab-free, and therefore
|
|
60
|
+
// executable under jest (the tint regressed twice behind a green suite).
|
|
74
61
|
|
|
75
62
|
const SECTION_LABELS: Record<SectionKey, string> = {
|
|
76
63
|
favourites: 'Favorites',
|
|
@@ -122,6 +109,10 @@ interface ITerminalCwdResponse {
|
|
|
122
109
|
// Conversation id the running claude is resuming, or null for a fresh
|
|
123
110
|
// (non-resumed) session. Lets reuse tell branches of one project apart.
|
|
124
111
|
session_id?: string | null;
|
|
112
|
+
// Colour of the conversation this terminal runs, from its own JSONL.
|
|
113
|
+
// Authoritative for the tint - the row carries only the representative
|
|
114
|
+
// conversation's colour (DEF-11).
|
|
115
|
+
color?: string | null;
|
|
125
116
|
}
|
|
126
117
|
|
|
127
118
|
// Drop any pre-v0.6.18 localStorage entries from previous schemes - they
|
|
@@ -822,29 +813,21 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
822
813
|
if (!info || !info.hasClaude || widget.isDisposed) {
|
|
823
814
|
return;
|
|
824
815
|
}
|
|
825
|
-
this._applyTerminalColour(
|
|
826
|
-
widget,
|
|
827
|
-
this._colourForTerminal(info, sessions)
|
|
828
|
-
);
|
|
816
|
+
this._applyTerminalColour(widget, colourForTerminal(info, sessions));
|
|
829
817
|
})
|
|
830
818
|
);
|
|
831
819
|
}
|
|
832
820
|
|
|
833
|
-
/**
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
* ``null`` when the terminal has no session name yet or the probe fails. */
|
|
821
|
+
/** Probe a terminal: runs Claude?, which conversation, its colour, its
|
|
822
|
+
* cwd(s). Probes EVERY terminal every pass - the launch cache is not
|
|
823
|
+
* consulted, it pins the launch-time conversation and goes stale on an
|
|
824
|
+
* in-place switch. Null when no session name yet, or the probe fails. */
|
|
838
825
|
private async _interrogateTerminal(widget: any): Promise<{
|
|
839
826
|
hasClaude: boolean;
|
|
840
827
|
sessionId: string | null;
|
|
828
|
+
color: string | null;
|
|
841
829
|
cwds: string[];
|
|
842
830
|
} | null> {
|
|
843
|
-
for (const entry of this._terminalsByPath.values()) {
|
|
844
|
-
if (entry.widget === widget && entry.sessionId) {
|
|
845
|
-
return { hasClaude: true, sessionId: entry.sessionId, cwds: [] };
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
831
|
const sessName: string | undefined = widget?.content?.session?.name;
|
|
849
832
|
if (typeof sessName !== 'string' || !sessName) {
|
|
850
833
|
return null;
|
|
@@ -857,6 +840,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
857
840
|
return {
|
|
858
841
|
hasClaude: !!data?.has_claude,
|
|
859
842
|
sessionId: data?.session_id ?? null,
|
|
843
|
+
color: data?.color ?? null,
|
|
860
844
|
cwds: Array.isArray(data?.cwds) ? data.cwds : []
|
|
861
845
|
};
|
|
862
846
|
} catch (_err) {
|
|
@@ -866,38 +850,6 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
866
850
|
}
|
|
867
851
|
}
|
|
868
852
|
|
|
869
|
-
/** Pick a terminal's tab colour from the session list. When the terminal runs
|
|
870
|
-
* a known conversation (its argv carried a ``--resume`` / ``--session-id``),
|
|
871
|
-
* match on that id ONLY: it takes the colour of the exact conversation, or -
|
|
872
|
-
* when that conversation is not the project's representative row - clears,
|
|
873
|
-
* rather than borrowing a sibling branch's colour. The cwd fallback is used
|
|
874
|
-
* ONLY for a terminal with no conversation id (a bare ``claude`` / ``-c``),
|
|
875
|
-
* matching the project the terminal is cwd'd in and preferring the longest
|
|
876
|
-
* matching path so a nested project wins over its parent. Returns null (clear)
|
|
877
|
-
* when no row matches or the matched row has no colour. */
|
|
878
|
-
private _colourForTerminal(
|
|
879
|
-
info: { sessionId: string | null; cwds: string[] },
|
|
880
|
-
sessions: ISession[]
|
|
881
|
-
): string | null {
|
|
882
|
-
if (info.sessionId) {
|
|
883
|
-
const bySession = sessions.find(s => s.session_id === info.sessionId);
|
|
884
|
-
return bySession ? (bySession.color ?? null) : null;
|
|
885
|
-
}
|
|
886
|
-
let best: ISession | undefined;
|
|
887
|
-
let bestLen = -1;
|
|
888
|
-
for (const raw of info.cwds) {
|
|
889
|
-
const cwd = raw.replace(/\/+$/, '');
|
|
890
|
-
for (const s of sessions) {
|
|
891
|
-
const p = s.project_path.replace(/\/+$/, '');
|
|
892
|
-
if ((cwd === p || cwd.startsWith(p + '/')) && p.length > bestLen) {
|
|
893
|
-
best = s;
|
|
894
|
-
bestLen = p.length;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
return best ? (best.color ?? null) : null;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
853
|
/** Find an open terminal running the wanted conversation, or null.
|
|
902
854
|
*
|
|
903
855
|
* Matches on the conversation id ALONE. The backend reads each terminal's
|