Repository navigation
Expand file tree
/
Copy pathstatusline.js
More file actions
190 lines (170 loc) · 7.96 KB
/
Copy pathstatusline.js
File metadata and controls
190 lines (170 loc) · 7.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// Dual-compatible status line for both Claude Code and GitHub Copilot CLI.
// One script, one set of defensive field lookups, works as the statusLine
// command for either tool — point it at whichever settings.json applies.
//
// Claude Code's stdin JSON schema is stable and documented (see
// https://code.claude.com/docs/en/statusline): context_window.total_input_tokens,
// context_window.remaining_percentage, model.display_name, cwd.
//
// GitHub Copilot CLI's statusLine command receives a different, and less
// formally documented, JSON schema. Field names below were confirmed from:
// - `copilot help config` (`statusLine` section)
// - EncodeTS/copilot-statusline (real third-party Copilot CLI statusline
// implementation) requiring `current_context_tokens` /
// `displayed_context_limit` under `context_window`
// - blog.madkoo.net's defensive field-probing statusline script, which
// enumerates every field-name variant seen across Copilot CLI versions
// Field access below stays defensive (tries several possible paths) so the
// same script resolves correctly against either schema without modification.
//
// Colors below use 24-bit truecolor SGR codes tuned to be vivid/"neon" on
// a pure-black background, matching the high-contrast treatment applied to
// the rest of the CLI by copilot-hc.js (see README.md). They read fine on
// their own too, since the statusline command's stdout is only ever piped
// through copilot.exe (and, if installed, the copilot-hc.js wrapper).
// Hard exit after 3s in case stdin never closes (prevents process accumulation)
setTimeout(() => process.exit(0), 3000).unref();
// Neon truecolor palette (foreground only; "0m" still resets to default).
const FG = {
magenta: '\x1b[38;2;255;70;255m', // model name
cyan: '\x1b[38;2;70;255;255m', // token count
green: '\x1b[38;2;70;255;110m', // context % used: low
yellow: '\x1b[38;2;255;255;70m', // context % used: medium
orange: '\x1b[38;2;255;170;40m', // context % used: high / cwd
red: '\x1b[38;2;255;60;90m', // context % used: critical / AC!!
teal: '\x1b[38;2;0;255;213m', // off-peak marker
gray: '\x1b[38;2;220;224;230m', // separators
reset: '\x1b[0m',
};
let d = '';
process.stdin.on('data', c => d += c);
process.stdin.on('end', () => {
let tok = '';
let compactStr = '';
let modelStr = '';
let ctxPct = '';
let cwdStr = '';
try {
const j = JSON.parse(d);
const cw = j?.context_window || j?.contextWindow || {};
// Total tokens used in the current context window. rawN stays undefined
// when none of these fields exist at all, so a legitimate zero still
// renders (only a payload with none of these fields omits the segment).
const rawN = cw.current_context_tokens ?? cw.currentContextTokens ?? cw.total_input_tokens
?? j?.currentTokens ?? j?.usage?.input_tokens ?? j?.usage?.inputTokens;
const n = rawN ?? 0;
// Context window size, to compute a "% used" figure.
const limit = cw.displayed_context_limit ?? cw.displayedContextLimit ?? cw.limit
?? cw.total_tokens ?? cw.totalTokens;
// Some payloads report "% used" directly; others only report "%
// remaining" (Claude-style); fall back to computing from tokens/limit.
let usedPct = cw.used_percentage ?? cw.usedPercentage;
if (usedPct === undefined && typeof cw.remaining_percentage === 'number') {
usedPct = 100 - cw.remaining_percentage;
}
if (usedPct === undefined && typeof n === 'number' && typeof limit === 'number' && limit > 0) {
usedPct = (n / limit) * 100;
}
// Model: may appear as a plain string or an object with a display name,
// under several different top-level keys depending on CLI version.
const modelRaw = j?.model ?? j?.currentModel ?? j?.selectedModel ?? j?.session?.selectedModel;
if (modelRaw && typeof modelRaw === 'object') {
modelStr = modelRaw.display_name || modelRaw.displayName || modelRaw.name || modelRaw.id || '';
} else if (typeof modelRaw === 'string') {
modelStr = modelRaw;
}
// Reasoning effort (Claude Code only; absent from Copilot CLI's schema,
// so this quietly no-ops there). Abbreviated to match the short labels
// Claude Code itself uses for /model's effort picker.
const EFFORT_ABBREV = { low: 'low', medium: 'md', high: 'h', xhigh: 'xh', max: 'MX' };
const effortRaw = j?.effort?.level ?? j?.effortLevel;
if (typeof effortRaw === 'string' && effortRaw && modelStr) {
modelStr += ' (' + (EFFORT_ABBREV[effortRaw.toLowerCase()] || effortRaw) + ')';
}
const rawCwd = j?.cwd || j?.workspace?.current_dir || j?.workspace?.currentDir || '';
cwdStr = rawCwd ? rawCwd.split(/[\\/]/).filter(Boolean).pop() || rawCwd : '';
// Tiered by absolute token count: green under 250K, yellow under 1M,
// red (M-scale) at 1M+. total_input_tokens is normally bounded by the
// context window (~200K), so the yellow/red tiers only fire on
// schemas/situations that report a larger, less-bounded figure.
if (n >= 1000000) {
tok = FG.red + (n / 1000000).toFixed(2) + 'M tokens' + FG.reset;
} else if (n >= 1000) {
const color = n >= 250000 ? FG.yellow : FG.green;
tok = color + Math.floor(n / 1000) + 'K tokens' + FG.reset;
} else if (n > 0) {
tok = FG.green + '<1K tokens' + FG.reset;
} else if (rawN !== undefined) {
tok = FG.green + '0 tokens' + FG.reset;
}
// Context % used with color tiers
if (usedPct !== undefined) {
const used = Math.round(usedPct);
let color;
if (used >= 95) color = FG.red;
else if (used >= 80) color = FG.orange;
else if (used >= 60) color = FG.yellow;
else color = FG.green;
ctxPct = color + used + '%' + FG.reset;
// auto-compact warning when remaining <= 20%
const rem = 100 - used;
if (rem <= 20) {
let acColor;
if (rem < 5) acColor = FG.red;
else if (rem < 9) acColor = FG.orange;
else acColor = FG.yellow;
compactStr = acColor + 'AC!!' + FG.reset;
}
}
} catch (e) {}
// Peak: 5-11am Pacific Time, Mon-Fri (DST-aware via America/Los_Angeles)
const ptParts = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/Los_Angeles',
hour: 'numeric',
minute: 'numeric',
hour12: false,
weekday: 'short',
}).formatToParts(new Date());
const getNum = t => {
const p = ptParts.find(p => p.type === t);
return p ? parseInt(p.value, 10) : 0;
};
const ptHour = getNum('hour') % 24;
const ptMinute = getNum('minute');
const ptWeekday = ptParts.find(p => p.type === 'weekday').value;
const isWeekday = ptWeekday !== 'Sat' && ptWeekday !== 'Sun';
const isPeak = isWeekday && ptHour >= 5 && ptHour < 11;
// Hours until next state change
let hoursToChange;
if (isPeak) {
hoursToChange = 11 - ptHour - ptMinute / 60;
} else if (isWeekday && ptHour < 5) {
hoursToChange = 5 - ptHour - ptMinute / 60;
} else {
const hoursToMidnight = 24 - ptHour - ptMinute / 60;
const wdOrder = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
let nextIdx = (wdOrder.indexOf(ptWeekday) + 1) % 7;
let extraDays = 0;
while (nextIdx === 0 || nextIdx === 6) {
extraDays++;
nextIdx = (nextIdx + 1) % 7;
}
hoursToChange = hoursToMidnight + extraDays * 24 + 5;
}
const totalMin = Math.round(hoursToChange * 60);
const h = Math.floor(totalMin / 60);
const m = totalMin % 60;
const dur = h === 0 ? m + 'm' : (m === 0 ? h + 'h' : h + 'h ' + m + 'm');
const peak = isPeak
? FG.yellow + 'PEAK' + FG.reset + ' (' + dur + ')'
: FG.teal + '🌙' + FG.reset + ' (' + dur + ')';
const parts = [];
if (modelStr) parts.push(FG.magenta + modelStr + FG.reset);
if (ctxPct) parts.push(ctxPct);
if (compactStr) parts.push(compactStr);
parts.push(peak);
if (cwdStr) parts.push(FG.orange + cwdStr + FG.reset);
if (tok) parts.push(tok);
process.stdout.write(parts.join(' ' + FG.gray + '|' + FG.reset + ' '));
process.exit(0);
});