Skip to content

Commit 794b0c1

Browse files
committed
feat: add session-color patch for env-based prompt bar color
Set the session prompt bar color via TWEAKCC_SESSION_COLOR env var. Intended for use in wrapper scripts (e.g. ccw) that determine color based on the working directory before launching Claude Code. Valid values: red, blue, green, yellow, purple, orange, pink, cyan. Invalid or unset values are silently ignored (no color applied). How it works: Claude Code has two app state initialization sites — the CLI bootstrap (which builds initialState inline with dynamic values like effortValue, fastMode) and the default state factory u3H() used as fallback. Neither includes standaloneAgentContext, so it defaults to undefined and the prompt bar shows no color. The patch injects a standaloneAgentContext IIFE into both sites that reads TWEAKCC_SESSION_COLOR at startup, validates it against the hardcoded allowed list, and returns {name:"",color:value} or void 0. This is the same state shape that /color sets interactively. On session resume, Claude Code restores the color from the session file, so the env var only affects new sessions.
1 parent 88b8ed0 commit 794b0c1

3 files changed

Lines changed: 129 additions & 0 deletions

File tree

src/patches/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import { writeWorktreeMode } from './worktreeMode';
7373
import { writeAllowCustomAgentModels } from './allowCustomAgentModels';
7474
import { writeVoiceMode } from './voiceMode';
7575
import { writeChannelsMode } from './channelsMode';
76+
import { writeSessionColor } from './sessionColor';
7677
import {
7778
restoreNativeBinaryFromBackup,
7879
restoreClijsFromBackup,
@@ -174,6 +175,13 @@ const PATCH_DEFINITIONS = [
174175
group: PatchGroup.ALWAYS_APPLIED,
175176
description: `Statusline updates will be properly throttled instead of queued (or debounced)`,
176177
},
178+
{
179+
id: 'session-color',
180+
name: 'Session color from env',
181+
group: PatchGroup.ALWAYS_APPLIED,
182+
description:
183+
'Set session prompt bar color via TWEAKCC_SESSION_COLOR env var',
184+
},
177185
// Misc Configurable
178186
{
179187
id: 'model-customizations',
@@ -888,6 +896,9 @@ export const applyCustomization = async (
888896
fn: c => writeChannelsMode(c),
889897
condition: !!config.settings.misc?.enableChannelsMode,
890898
},
899+
'session-color': {
900+
fn: c => writeSessionColor(c),
901+
},
891902
};
892903

893904
// ==========================================================================

src/patches/sessionColor.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { writeSessionColor } from './sessionColor';
3+
4+
const makeCLIState = () =>
5+
'effortValue:oR(w.effort),' +
6+
'activeOverlays:new Set,fastMode:cP8(N5),' +
7+
'...(uF()&&d1&&{advisorModel:d1})';
8+
9+
const makeDefaultState = () =>
10+
'effortValue:void 0,' + 'activeOverlays:new Set,fastMode:!1}';
11+
12+
const makeBoth = () =>
13+
'first{' + makeDefaultState() + 'second{' + makeCLIState();
14+
15+
describe('sessionColor', () => {
16+
describe('writeSessionColor', () => {
17+
it('should inject into CLI initialState', () => {
18+
const result = writeSessionColor(makeCLIState());
19+
expect(result).not.toBeNull();
20+
expect(result).toContain('TWEAKCC_SESSION_COLOR');
21+
expect(result).toContain('{name:"",color:__c}');
22+
});
23+
24+
it('should inject into default app state', () => {
25+
const result = writeSessionColor(makeDefaultState());
26+
expect(result).not.toBeNull();
27+
expect(result).toContain('TWEAKCC_SESSION_COLOR');
28+
});
29+
30+
it('should patch both locations when both present', () => {
31+
const result = writeSessionColor(makeBoth())!;
32+
expect(result).not.toBeNull();
33+
const count = (result.match(/TWEAKCC_SESSION_COLOR/g) || []).length;
34+
expect(count).toBe(2);
35+
});
36+
37+
it('should validate color against allowed list', () => {
38+
const result = writeSessionColor(makeCLIState())!;
39+
expect(result).toContain('.includes(__c)');
40+
expect(result).toContain('"green"');
41+
expect(result).toContain('"cyan"');
42+
});
43+
44+
it('should be idempotent', () => {
45+
const first = writeSessionColor(makeBoth())!;
46+
const second = writeSessionColor(first)!;
47+
expect(second).toBe(first);
48+
});
49+
50+
it('should return null when no pattern found', () => {
51+
const result = writeSessionColor('not a valid file');
52+
expect(result).toBeNull();
53+
});
54+
});
55+
});

src/patches/sessionColor.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { showDiff } from './index';
2+
import { debug } from '../utils';
3+
4+
const VALID_COLORS = [
5+
'red',
6+
'blue',
7+
'green',
8+
'yellow',
9+
'purple',
10+
'orange',
11+
'pink',
12+
'cyan',
13+
];
14+
15+
const INJECTION =
16+
`,standaloneAgentContext:` +
17+
`(()=>{` +
18+
`let __c=process.env.TWEAKCC_SESSION_COLOR;` +
19+
`return __c&&${JSON.stringify(VALID_COLORS)}.includes(__c)` +
20+
`?{name:"",color:__c}` +
21+
`:void 0` +
22+
`})()`;
23+
24+
export const writeSessionColor = (oldFile: string): string | null => {
25+
if (oldFile.includes('TWEAKCC_SESSION_COLOR')) {
26+
return oldFile;
27+
}
28+
29+
const patterns = [
30+
/,activeOverlays:new Set,fastMode:[$\w]+\([$\w]+\)/,
31+
/,activeOverlays:new Set,fastMode:!1\}/,
32+
];
33+
34+
let result = oldFile;
35+
let patched = false;
36+
37+
for (const pattern of patterns) {
38+
const match = result.match(pattern);
39+
if (!match || match.index === undefined) continue;
40+
41+
const replacement = INJECTION + match[0];
42+
result =
43+
result.slice(0, match.index) +
44+
replacement +
45+
result.slice(match.index + match[0].length);
46+
47+
showDiff(
48+
oldFile,
49+
result,
50+
replacement,
51+
match.index,
52+
match.index + match[0].length
53+
);
54+
patched = true;
55+
}
56+
57+
if (!patched) {
58+
debug('patch: sessionColor: failed to find app state init patterns');
59+
return null;
60+
}
61+
62+
return result;
63+
};

0 commit comments

Comments
 (0)