claude-usage-limits 1.11.5 → 1.11.6
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/usage-limits/scripts/activity.js +4 -0
- package/skills/usage-limits/scripts/bars.js +16 -5
- package/skills/usage-limits/scripts/brief.js +19 -6
- package/skills/usage-limits/scripts/codex.js +107 -1
- package/skills/usage-limits/scripts/feed.js +29 -10
- package/skills/usage-limits/scripts/panel.js +32 -16
- package/skills/usage-limits/scripts/pulse.js +6 -4
- package/skills/usage-limits/scripts/view.js +17 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.11.
|
|
4
|
+
"version": "1.11.6",
|
|
5
5
|
"description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Ridelink",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.6",
|
|
4
4
|
"description": "Reports how much of your Codex usage limit is left as turns of work rather than a percentage, prices a job before you start it, and counts the other agents sharing the same budget.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ridelink",
|
package/README.md
CHANGED
|
@@ -864,7 +864,7 @@ test/ node --test, no dependencies
|
|
|
864
864
|
node --test
|
|
865
865
|
```
|
|
866
866
|
|
|
867
|
-
|
|
867
|
+
480 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
868
868
|
the status line, the before-prompt line, the mid-turn pulse, the after-reply tally and the session history, job forecasting,
|
|
869
869
|
per-project attribution, the Codex reader and its installer, the CLI,
|
|
870
870
|
packaging, and the settings save/restore.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.6",
|
|
4
4
|
"description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -63,6 +63,7 @@ function mark(state, sessionId, extra, now) {
|
|
|
63
63
|
// The keyword is per prompt, so a Stop keeps what the prompt said and the
|
|
64
64
|
// next prompt says again.
|
|
65
65
|
ultracode: extra && typeof extra.ultracode === 'boolean' ? extra.ultracode : Boolean(previous.ultracode),
|
|
66
|
+
ultrathink: extra && typeof extra.ultrathink === 'boolean' ? extra.ultrathink : Boolean(previous.ultrathink),
|
|
66
67
|
};
|
|
67
68
|
if (extra && extra.model) entry.model = String(extra.model);
|
|
68
69
|
else if (previous.model) entry.model = previous.model;
|
|
@@ -107,6 +108,7 @@ function summarise(all, now) {
|
|
|
107
108
|
return {
|
|
108
109
|
working: Boolean(working),
|
|
109
110
|
ultracode: Boolean(working ? working.ultracode : newest && newest.ultracode),
|
|
111
|
+
ultrathink: Boolean(working ? working.ultrathink : newest && newest.ultrathink),
|
|
110
112
|
model: (working && working.model) || (newest && newest.model) || null,
|
|
111
113
|
at: newest ? newest.at : null,
|
|
112
114
|
};
|
|
@@ -134,6 +136,7 @@ function combine(sources, now, windowMs) {
|
|
|
134
136
|
state: 'idle',
|
|
135
137
|
stateAt: null,
|
|
136
138
|
ultracode: false,
|
|
139
|
+
ultrathink: false,
|
|
137
140
|
model: null,
|
|
138
141
|
modelName: null,
|
|
139
142
|
effort: null,
|
|
@@ -156,6 +159,7 @@ function combine(sources, now, windowMs) {
|
|
|
156
159
|
row.stateAt = m.at;
|
|
157
160
|
row.state = m.state === 'working' && at - m.at <= within ? 'working' : 'idle';
|
|
158
161
|
row.ultracode = Boolean(m.ultracode);
|
|
162
|
+
row.ultrathink = Boolean(m.ultrathink);
|
|
159
163
|
if (m.model && !row.model) row.model = m.model;
|
|
160
164
|
}
|
|
161
165
|
for (const id of Object.keys(src.feed || {})) {
|
|
@@ -166,10 +166,18 @@ function bar(percent, width, options) {
|
|
|
166
166
|
const fillGlyph = opts.ascii ? FILL_ASCII : FILL;
|
|
167
167
|
const emptyGlyph = opts.ascii ? EMPTY_ASCII : EMPTY;
|
|
168
168
|
const mode = opts.mode || 'none';
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
// The filled cells: one colour for the level, or under ultracode the
|
|
170
|
+
// rainbow sliding along the bar, or under ultrathink the purple with a
|
|
171
|
+
// moving highlight, the way Claude Code paints those two words.
|
|
172
|
+
let fill;
|
|
173
|
+
if (opts.style === 'rainbow' && mode !== 'none') {
|
|
174
|
+
fill = rainbow(fillGlyph.repeat(filled), opts.tick, { mode, reduced: opts.reduced });
|
|
175
|
+
} else if (opts.style === 'ultra' && mode !== 'none') {
|
|
176
|
+
fill = shimmer(fillGlyph.repeat(filled), opts.tick, THEME.ultra, THEME.ultraShimmer, { mode, reduced: opts.reduced });
|
|
177
|
+
} else {
|
|
178
|
+
fill = paint(fillGlyph.repeat(filled), levelColour(opts.level || level(pct)), mode);
|
|
179
|
+
}
|
|
180
|
+
return fill + paint(emptyGlyph.repeat(cells - filled), THEME.empty, mode);
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
function spinner(tick, options) {
|
|
@@ -231,8 +239,11 @@ function effortColour(name) {
|
|
|
231
239
|
return { rgb: THEME.permission, shimmer: null, rainbow: false };
|
|
232
240
|
case 'xhigh':
|
|
233
241
|
return { rgb: THEME.ultra, shimmer: THEME.ultraShimmer, rainbow: false };
|
|
234
|
-
|
|
242
|
+
// Claude Code's effort picker paints Ultracode in the purple; max is the
|
|
243
|
+
// one it animates in the rainbow.
|
|
235
244
|
case 'ultracode':
|
|
245
|
+
return { rgb: THEME.ultra, shimmer: THEME.ultraShimmer, rainbow: false };
|
|
246
|
+
case 'max':
|
|
236
247
|
return { rgb: THEME.ultra, shimmer: THEME.ultraShimmer, rainbow: true };
|
|
237
248
|
default:
|
|
238
249
|
return { rgb: THEME.inactive, shimmer: null, rainbow: false };
|
|
@@ -621,8 +621,12 @@ async function run(now, hookInput) {
|
|
|
621
621
|
'working',
|
|
622
622
|
sessionId,
|
|
623
623
|
{
|
|
624
|
-
|
|
625
|
-
|
|
624
|
+
// Only ultrathink, and only as a whole word: it is a real directive in
|
|
625
|
+
// the prompt. Ultracode is an effort level, read from the setting the
|
|
626
|
+
// agent reports, never from the text - the word turns up in ordinary
|
|
627
|
+
// requests, and these marks are read by every panel on the machine.
|
|
628
|
+
ultrathink: Boolean(
|
|
629
|
+
hookInput && typeof hookInput.prompt === 'string' && /\bultrathink\b/i.test(hookInput.prompt)
|
|
626
630
|
),
|
|
627
631
|
},
|
|
628
632
|
now
|
|
@@ -635,8 +639,17 @@ async function run(now, hookInput) {
|
|
|
635
639
|
// one on disk is older than a few minutes. Offline or signed out this is
|
|
636
640
|
// one quick failure and then a widening backoff, never a wait on every
|
|
637
641
|
// prompt; USAGE_LIMITS_FETCH=off turns it off.
|
|
638
|
-
|
|
639
|
-
|
|
642
|
+
try {
|
|
643
|
+
if (usage.isCodex()) {
|
|
644
|
+
// Codex only writes its meter when it makes a request, so between turns
|
|
645
|
+
// the newest figure can be half an hour old. Ask it, the way /status
|
|
646
|
+
// does, when the reading has aged.
|
|
647
|
+
await require('./codex.js').refreshIfStale({
|
|
648
|
+
now,
|
|
649
|
+
maxAgeMs: config.refreshSeconds * SECOND,
|
|
650
|
+
timeoutMs: REFRESH_TIMEOUT_MS,
|
|
651
|
+
});
|
|
652
|
+
} else {
|
|
640
653
|
const cached = usage.collect(now);
|
|
641
654
|
await live.refreshIfStale({
|
|
642
655
|
now,
|
|
@@ -645,9 +658,9 @@ async function run(now, hookInput) {
|
|
|
645
658
|
accountUuid: usage.accountUuid(),
|
|
646
659
|
timeoutMs: REFRESH_TIMEOUT_MS,
|
|
647
660
|
});
|
|
648
|
-
} catch (err) {
|
|
649
|
-
// The reading on disk is still there.
|
|
650
661
|
}
|
|
662
|
+
} catch (err) {
|
|
663
|
+
// The reading on disk is still there.
|
|
651
664
|
}
|
|
652
665
|
const base = usage.collect(now);
|
|
653
666
|
if (!base.utilization) return '';
|
|
@@ -628,8 +628,109 @@ function meterFromDisk() {
|
|
|
628
628
|
return null;
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
+
// Where a live reading taken by refreshIfStale() is kept, so the hooks and the
|
|
632
|
+
// report see it without asking Codex again.
|
|
633
|
+
function liveFile() {
|
|
634
|
+
return path.join(homeDir(), 'usage-limits-codex-live.json');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function readLiveMeter() {
|
|
638
|
+
let parsed;
|
|
639
|
+
try {
|
|
640
|
+
parsed = JSON.parse(fs.readFileSync(liveFile(), 'utf8'));
|
|
641
|
+
} catch (err) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
645
|
+
if (!Number.isFinite(parsed.at) || !parsed.meter || typeof parsed.meter !== 'object') return null;
|
|
646
|
+
return parsed;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function writeLiveMeter(reading) {
|
|
650
|
+
try {
|
|
651
|
+
const file = liveFile();
|
|
652
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
653
|
+
const temp = file + '.' + process.pid + '.usage-limits-tmp';
|
|
654
|
+
fs.writeFileSync(temp, JSON.stringify(reading), 'utf8');
|
|
655
|
+
fs.renameSync(temp, file);
|
|
656
|
+
return true;
|
|
657
|
+
} catch (err) {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function attemptFile() {
|
|
663
|
+
return path.join(homeDir(), 'usage-limits-codex-fetch.json');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Ask Codex for the meter, but only when the newest reading on disk has aged,
|
|
667
|
+
// and never twice in quick succession. Codex writes its meter into a rollout
|
|
668
|
+
// only when it makes a request, so between turns the newest reading can be
|
|
669
|
+
// half an hour old: the agent then plans against a figure from before the work
|
|
670
|
+
// it just did, says the limit is fine, and hits it. This is what closes that
|
|
671
|
+
// gap, the same way the Claude side takes a fresh /usage reading.
|
|
672
|
+
async function refreshIfStale(options) {
|
|
673
|
+
const opts = options || {};
|
|
674
|
+
const now = Number.isFinite(opts.now) ? opts.now : Date.now();
|
|
675
|
+
const maxAgeMs = Number.isFinite(opts.maxAgeMs) ? opts.maxAgeMs : 3 * MINUTE;
|
|
676
|
+
const env = opts.env || process.env;
|
|
677
|
+
if (opts.fetch === false || String(env.USAGE_LIMITS_FETCH || '').toLowerCase() === 'off') {
|
|
678
|
+
return { reading: readLiveMeter(), skipped: 'disabled' };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const onDisk = meterFromDisk();
|
|
682
|
+
const live = readLiveMeter();
|
|
683
|
+
const newestAt = Math.max(onDisk && Number.isFinite(onDisk.at) ? onDisk.at : 0, live ? live.at : 0);
|
|
684
|
+
if (newestAt > 0 && now - newestAt < maxAgeMs) return { reading: live, skipped: 'fresh' };
|
|
685
|
+
|
|
686
|
+
let attempt = null;
|
|
687
|
+
try {
|
|
688
|
+
attempt = JSON.parse(fs.readFileSync(attemptFile(), 'utf8'));
|
|
689
|
+
} catch (err) {
|
|
690
|
+
attempt = null;
|
|
691
|
+
}
|
|
692
|
+
const sinceAttempt = attempt && Number.isFinite(attempt.attemptedAtMs) ? now - attempt.attemptedAtMs : null;
|
|
693
|
+
if (sinceAttempt !== null && Number.isFinite(attempt.delayMs) && sinceAttempt >= -MINUTE && sinceAttempt < attempt.delayMs) {
|
|
694
|
+
return { reading: live, skipped: 'backoff' };
|
|
695
|
+
}
|
|
696
|
+
// Claim the attempt before making it: Codex's app-server takes about a
|
|
697
|
+
// second, and several hooks can fire at once.
|
|
698
|
+
const claim = (delayMs, kind) => {
|
|
699
|
+
try {
|
|
700
|
+
fs.writeFileSync(attemptFile(), JSON.stringify({ attemptedAtMs: now, delayMs, kind }), 'utf8');
|
|
701
|
+
} catch (err) {
|
|
702
|
+
// One extra attempt is survivable.
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
claim(Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : 8000, 'inflight');
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
const reading = await refresh({ codexPath: opts.codexPath, timeoutMs: opts.timeoutMs });
|
|
709
|
+
if (!reading || !reading.meter) {
|
|
710
|
+
claim(maxAgeMs, 'empty');
|
|
711
|
+
return { reading: live, skipped: null, error: 'no meter' };
|
|
712
|
+
}
|
|
713
|
+
writeLiveMeter(reading);
|
|
714
|
+
claim(maxAgeMs, 'ok');
|
|
715
|
+
return { reading, skipped: null };
|
|
716
|
+
} catch (err) {
|
|
717
|
+
// Codex missing or not answering: back off rather than pay for it again on
|
|
718
|
+
// every prompt.
|
|
719
|
+
claim(Math.min(10 * MINUTE, Math.max(2 * MINUTE, maxAgeMs * 2)), (err && err.code) || 'error');
|
|
720
|
+
return { reading: live, skipped: null, error: (err && err.code) || 'error' };
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
631
724
|
function collect(now, options) {
|
|
632
|
-
|
|
725
|
+
// The newest of the three: a reading handed in, one taken by the hooks, or
|
|
726
|
+
// the newest one Codex happened to write into a rollout.
|
|
727
|
+
const given = options && options.meter;
|
|
728
|
+
const live = readLiveMeter();
|
|
729
|
+
const disk = meterFromDisk();
|
|
730
|
+
const best =
|
|
731
|
+
given ||
|
|
732
|
+
(live && (!disk || !Number.isFinite(disk.at) || live.at > disk.at) ? live : disk);
|
|
733
|
+
const found = best;
|
|
633
734
|
const mapped = found ? utilizationFrom(found.meter) : null;
|
|
634
735
|
const plan = planFrom(mapped && mapped.planType);
|
|
635
736
|
|
|
@@ -847,6 +948,11 @@ module.exports = {
|
|
|
847
948
|
planFrom,
|
|
848
949
|
latestMeter,
|
|
849
950
|
meterFromDisk,
|
|
951
|
+
liveFile,
|
|
952
|
+
attemptFile,
|
|
953
|
+
readLiveMeter,
|
|
954
|
+
writeLiveMeter,
|
|
955
|
+
refreshIfStale,
|
|
850
956
|
calibrate,
|
|
851
957
|
MIN_POINTS_MOVED,
|
|
852
958
|
MIN_SAMPLE_TURNS,
|
|
@@ -34,6 +34,9 @@ const WORKING_GAP_MS = 4000;
|
|
|
34
34
|
// A previous status line gets this long, then we go on without it.
|
|
35
35
|
const CHAIN_TIMEOUT_MS = 2000;
|
|
36
36
|
const STDIN_WAIT_MS = 500;
|
|
37
|
+
// How long a display keeps describing the session it chose before it will
|
|
38
|
+
// follow a different one.
|
|
39
|
+
const STICKY_QUIET_MS = 5 * 60 * 1000;
|
|
37
40
|
// Claude Code draws the status line inside its own margins, a few columns
|
|
38
41
|
// narrower than COLUMNS, and clips what does not fit.
|
|
39
42
|
const STATUSLINE_MARGIN = 4;
|
|
@@ -86,8 +89,10 @@ function gapMeansWorking(settings) {
|
|
|
86
89
|
// spin this one's line.
|
|
87
90
|
function ownState(marks, sessionId, now) {
|
|
88
91
|
const mine = sessionId && marks ? marks[sessionId] : null;
|
|
89
|
-
if (!mine || !Number.isFinite(mine.at) || now - mine.at > activity.STALE_MS)
|
|
90
|
-
|
|
92
|
+
if (!mine || !Number.isFinite(mine.at) || now - mine.at > activity.STALE_MS) {
|
|
93
|
+
return { working: false, ultracode: false, ultrathink: false };
|
|
94
|
+
}
|
|
95
|
+
return { working: mine.state === 'working', ultracode: Boolean(mine.ultracode), ultrathink: Boolean(mine.ultrathink) };
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
function number(value) {
|
|
@@ -141,6 +146,22 @@ function newest(all) {
|
|
|
141
146
|
return best;
|
|
142
147
|
}
|
|
143
148
|
|
|
149
|
+
// Which session a display with no session of its own should describe.
|
|
150
|
+
//
|
|
151
|
+
// "Whichever moved last" reads badly with two windows open: two Claudes on the
|
|
152
|
+
// same model at different efforts made the line flip between ultracode and
|
|
153
|
+
// xhigh every few seconds, which is noise, not news. So a display sticks to
|
|
154
|
+
// the session it is already describing until that one has been quiet for a
|
|
155
|
+
// while, and only then moves to the newest.
|
|
156
|
+
function stickySlot(all, previousId, now, quietMs) {
|
|
157
|
+
const slots = all || {};
|
|
158
|
+
const quiet = Number.isFinite(quietMs) ? quietMs : STICKY_QUIET_MS;
|
|
159
|
+
const at = Number.isFinite(now) ? now : Date.now();
|
|
160
|
+
const held = previousId ? slots[previousId] : null;
|
|
161
|
+
if (held && Number.isFinite(held.at) && at - held.at <= quiet) return held;
|
|
162
|
+
return newest(slots);
|
|
163
|
+
}
|
|
164
|
+
|
|
144
165
|
function isWorking(slot, now) {
|
|
145
166
|
if (!slot || !Number.isFinite(slot.at) || !Number.isFinite(slot.prevAt)) return false;
|
|
146
167
|
return now - slot.at < WORKING_GAP_MS && slot.at - slot.prevAt < WORKING_GAP_MS;
|
|
@@ -166,13 +187,9 @@ function line(built, options) {
|
|
|
166
187
|
if (built.state === 'none') return bars.dim('usage: no reading yet', mode);
|
|
167
188
|
|
|
168
189
|
const glyph = built.working
|
|
169
|
-
?
|
|
170
|
-
? bars.rainbow(bars.spinner(tick, { ascii, reduced }), tick, { mode, reduced })
|
|
171
|
-
: bars.paint(bars.spinner(tick, { ascii, reduced }), bars.THEME.claude, mode)
|
|
190
|
+
? bars.paint(bars.spinner(tick, { ascii, reduced }), bars.THEME.claude, mode)
|
|
172
191
|
: bars.paint(ascii ? '*' : '✻', bars.THEME.claude, mode);
|
|
173
|
-
|
|
174
|
-
// the picker, so it is named here too, in the rainbow the picker uses.
|
|
175
|
-
const effortName = built.ultracode ? 'ultracode' : built.effort;
|
|
192
|
+
const effortName = built.effort;
|
|
176
193
|
const effort = effortName ? bars.effortColour(effortName) : null;
|
|
177
194
|
const effortText = !effortName
|
|
178
195
|
? ''
|
|
@@ -187,7 +204,7 @@ function line(built, options) {
|
|
|
187
204
|
const label = shortLabel(row, shorter);
|
|
188
205
|
const percent = row.level === 'fill' ? row.percentText : bars.paint(row.percentText, bars.levelColour(row.level), mode);
|
|
189
206
|
if (!width || row.percent === null) return label + ' ' + percent;
|
|
190
|
-
return label + ' ' + bars.bar(row.percent, width, { mode, level: row.level, ascii }) + ' ' + percent;
|
|
207
|
+
return label + ' ' + bars.bar(row.percent, width, { mode, level: row.level, ascii, tick, reduced, style: built.style }) + ' ' + percent;
|
|
191
208
|
};
|
|
192
209
|
|
|
193
210
|
const attempts = [
|
|
@@ -346,7 +363,7 @@ async function main(argv) {
|
|
|
346
363
|
modelName: slot ? slot.modelName : null,
|
|
347
364
|
effort: slot ? slot.effort : null,
|
|
348
365
|
working: own.working || (gapMeansWorking(settings) && isWorking(slot, now)),
|
|
349
|
-
|
|
366
|
+
ultrathink: Boolean(own.ultrathink),
|
|
350
367
|
settingsModel: collected.settings ? collected.settings.model : null,
|
|
351
368
|
env,
|
|
352
369
|
});
|
|
@@ -379,6 +396,8 @@ module.exports = {
|
|
|
379
396
|
record,
|
|
380
397
|
newest,
|
|
381
398
|
isWorking,
|
|
399
|
+
stickySlot,
|
|
400
|
+
STICKY_QUIET_MS,
|
|
382
401
|
gapMeansWorking,
|
|
383
402
|
ownState,
|
|
384
403
|
line,
|
|
@@ -154,7 +154,9 @@ async function snapshot(options) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
const slots = onCodex ? {} : feed.readFeed();
|
|
157
|
-
|
|
157
|
+
// Stay with the session already being described, so two windows on the same
|
|
158
|
+
// model at different efforts do not make the header flip back and forth.
|
|
159
|
+
const slot = feed.stickySlot(slots, opts.sessionId, now);
|
|
158
160
|
const seen = onCodex ? { working: codexWorking(now), ultracode: false, model: null } : activity.summarise(activity.read(), now);
|
|
159
161
|
const settings = onCodex ? {} : settingsFor();
|
|
160
162
|
|
|
@@ -168,14 +170,24 @@ async function snapshot(options) {
|
|
|
168
170
|
headersAt: slot ? slot.headersAt : null,
|
|
169
171
|
model: (slot && slot.model) || seen.model || null,
|
|
170
172
|
modelName: slot ? slot.modelName : null,
|
|
171
|
-
|
|
173
|
+
// The status line is told the effort by Claude Code itself; the setting is
|
|
174
|
+
// the fallback, and it is what says "ultracode" when no status line is
|
|
175
|
+
// installed.
|
|
176
|
+
effort:
|
|
177
|
+
(slot && slot.effort) ||
|
|
178
|
+
(collected.settings && collected.settings.effortLevel && collected.settings.effortLevel !== 'default'
|
|
179
|
+
? collected.settings.effortLevel
|
|
180
|
+
: null),
|
|
172
181
|
working: seen.working || feed.isWorking(slot, now),
|
|
173
|
-
|
|
182
|
+
// Ultracode comes from the effort level above, not from here. Ultrathink
|
|
183
|
+
// is a word in a prompt, and the hooks record it per session.
|
|
184
|
+
ultrathink: Boolean(seen.ultrathink),
|
|
174
185
|
settingsModel: collected.settings ? collected.settings.model : null,
|
|
175
186
|
outcome,
|
|
176
187
|
env,
|
|
177
188
|
});
|
|
178
189
|
built.now = now;
|
|
190
|
+
built.sessionId = slot && slot.sessionId ? slot.sessionId : null;
|
|
179
191
|
built.host = onCodex ? 'codex' : 'claude';
|
|
180
192
|
built.title = onCodex ? 'Codex usage' : TITLE;
|
|
181
193
|
built.plan = collected.plan || null;
|
|
@@ -353,19 +365,17 @@ function render(built, options) {
|
|
|
353
365
|
const barWidth = Math.max(8, Math.min(50, columns - 6));
|
|
354
366
|
const animate = built.working && !reduced;
|
|
355
367
|
|
|
368
|
+
// The title is Claude's orange, shimmering while Claude works, and nothing
|
|
369
|
+
// else: the rainbow and the purple belong to the bars.
|
|
356
370
|
const glyph = built.working
|
|
357
|
-
?
|
|
358
|
-
? bars.rainbow(bars.spinner(tick, { ascii, reduced }), tick, { mode, reduced })
|
|
359
|
-
: bars.paint(bars.spinner(tick, { ascii, reduced }), bars.THEME.claude, mode)
|
|
371
|
+
? bars.paint(bars.spinner(tick, { ascii, reduced }), bars.THEME.claude, mode)
|
|
360
372
|
: bars.paint(ascii ? '*' : '✻', bars.THEME.claude, mode);
|
|
361
373
|
const titleText = built.title || TITLE;
|
|
362
|
-
const title =
|
|
363
|
-
? bars.
|
|
364
|
-
:
|
|
365
|
-
? bars.shimmer(titleText, tick, bars.THEME.claude, bars.THEME.claudeShimmer, { mode, reduced })
|
|
366
|
-
: bars.paint(titleText, bars.THEME.claude, mode);
|
|
374
|
+
const title = animate
|
|
375
|
+
? bars.shimmer(titleText, tick, bars.THEME.claude, bars.THEME.claudeShimmer, { mode, reduced })
|
|
376
|
+
: bars.paint(titleText, bars.THEME.claude, mode);
|
|
367
377
|
|
|
368
|
-
const effortName = built.
|
|
378
|
+
const effortName = built.effort;
|
|
369
379
|
const effort = effortName ? bars.effortColour(effortName) : null;
|
|
370
380
|
const effortText = !effortName
|
|
371
381
|
? ''
|
|
@@ -375,7 +385,10 @@ function render(built, options) {
|
|
|
375
385
|
? bars.shimmer(effortName, tick, effort.rgb, effort.shimmer, { mode, reduced })
|
|
376
386
|
: bars.paint(effortName, effort.rgb, mode);
|
|
377
387
|
const status = built.working ? 'working' : 'idle';
|
|
378
|
-
const
|
|
388
|
+
const thinking = built.ultrathink
|
|
389
|
+
? bars.rainbow('ultrathink', tick, { mode, reduced })
|
|
390
|
+
: '';
|
|
391
|
+
const who = [built.modelLabel, effortText, thinking, bars.dim(status, mode)].filter(Boolean).join(bars.dim(' · ', mode));
|
|
379
392
|
|
|
380
393
|
const head = [glyph + ' ' + bars.bold(title, mode), who];
|
|
381
394
|
const body = [];
|
|
@@ -385,7 +398,9 @@ function render(built, options) {
|
|
|
385
398
|
body.push({
|
|
386
399
|
lines: [
|
|
387
400
|
bars.bold(columns < 34 ? shortTitle(row) : row.title, mode),
|
|
388
|
-
(row.percent === null
|
|
401
|
+
(row.percent === null
|
|
402
|
+
? bars.paint((ascii ? '-' : '░').repeat(barWidth), bars.THEME.empty, mode)
|
|
403
|
+
: bars.bar(row.percent, barWidth, { mode, level: row.level, ascii, tick, reduced, style: built.style })) +
|
|
389
404
|
' ' +
|
|
390
405
|
percent,
|
|
391
406
|
subline(row, mode, { now, clock }),
|
|
@@ -687,7 +702,7 @@ async function interactive(args) {
|
|
|
687
702
|
// The reading runs beside the frames, never in front of them: a slow
|
|
688
703
|
// network must not freeze the spinner or the countdown.
|
|
689
704
|
state.fetching = true;
|
|
690
|
-
snapshot({ fetch: true, network: fetch, env, now })
|
|
705
|
+
snapshot({ fetch: true, network: fetch, env, now, sessionId: state.built ? state.built.sessionId : null })
|
|
691
706
|
.then((built) => {
|
|
692
707
|
state.built = built;
|
|
693
708
|
state.outcome = built.outcome;
|
|
@@ -715,6 +730,7 @@ async function interactive(args) {
|
|
|
715
730
|
now,
|
|
716
731
|
outcome: state.outcome,
|
|
717
732
|
pace: state.built ? state.built.pace : null,
|
|
733
|
+
sessionId: state.built ? state.built.sessionId : null,
|
|
718
734
|
});
|
|
719
735
|
} catch (err) {
|
|
720
736
|
// Keep the last frame; a transient read error is not worth a blank.
|
|
@@ -734,7 +750,7 @@ async function interactive(args) {
|
|
|
734
750
|
}
|
|
735
751
|
if (state.built) {
|
|
736
752
|
const tick = Math.floor(Date.now() / bars.TICK_MS);
|
|
737
|
-
const animating = (state.built.working || state.built.ultracode) && !reduced;
|
|
753
|
+
const animating = (state.built.working || state.built.ultracode || state.built.ultrathink) && !reduced;
|
|
738
754
|
if (state.dirty || (animating && tick !== state.lastTick) || now - state.lastCheck < FRAME_MS) {
|
|
739
755
|
state.lastTick = tick;
|
|
740
756
|
draw(Date.now());
|
|
@@ -134,8 +134,10 @@ async function run(now, hookInput) {
|
|
|
134
134
|
// A reading as old as the interval is replaced with the one Claude Code
|
|
135
135
|
// would take for /usage, so a turn that runs for an hour is measured
|
|
136
136
|
// against the account rather than against a guess from its own transcript.
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
try {
|
|
138
|
+
if (usage.isCodex()) {
|
|
139
|
+
await require('./codex.js').refreshIfStale({ now, maxAgeMs: every, timeoutMs: 4000 });
|
|
140
|
+
} else {
|
|
139
141
|
const cached = usage.collect(now);
|
|
140
142
|
await live.refreshIfStale({
|
|
141
143
|
now,
|
|
@@ -144,9 +146,9 @@ async function run(now, hookInput) {
|
|
|
144
146
|
accountUuid: usage.accountUuid(),
|
|
145
147
|
timeoutMs: 4000,
|
|
146
148
|
});
|
|
147
|
-
} catch (err) {
|
|
148
|
-
// The reading on disk is still there.
|
|
149
149
|
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// The reading on disk is still there.
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
const data = await usage.report(now, { sessionId });
|
|
@@ -195,7 +195,21 @@ function build(input) {
|
|
|
195
195
|
const state = !hasData ? 'none' : ageMs !== null && ageMs < LIVE_AGE_MS ? 'live' : 'cached';
|
|
196
196
|
|
|
197
197
|
const effort = opts.effort ? String(opts.effort).toLowerCase() : null;
|
|
198
|
-
|
|
198
|
+
// Ultracode is a level in Claude Code's own effort picker, painted purple
|
|
199
|
+
// there, so it comes from the level the agent reports and NEVER from a word
|
|
200
|
+
// in the prompt. Reading it from the text was wrong twice over: "ultracode"
|
|
201
|
+
// appears in ordinary requests, and the marks are machine-wide, so one
|
|
202
|
+
// session mentioning it turned every panel purple while the effort was
|
|
203
|
+
// xhigh.
|
|
204
|
+
const ultracode = effort === 'ultracode';
|
|
205
|
+
// Ultrathink is a word in the prompt, and Claude Code paints that word in
|
|
206
|
+
// the rainbow, so the bars do the same.
|
|
207
|
+
const ultrathink = Boolean(opts.ultrathink);
|
|
208
|
+
// What the bars do: the rainbow for ultrathink and for max effort, the
|
|
209
|
+
// purple gradient of the effort picker for ultracode, their own level
|
|
210
|
+
// colour otherwise. The title is never painted in either; it stays the
|
|
211
|
+
// Claude orange, because the bar is the thing being reported on.
|
|
212
|
+
const style = ultrathink || effort === 'max' ? 'rainbow' : ultracode ? 'ultra' : null;
|
|
199
213
|
|
|
200
214
|
return {
|
|
201
215
|
rows,
|
|
@@ -205,6 +219,8 @@ function build(input) {
|
|
|
205
219
|
modelLabel: opts.modelName || bars.prettyModel(model),
|
|
206
220
|
effort,
|
|
207
221
|
ultracode,
|
|
222
|
+
ultrathink,
|
|
223
|
+
style,
|
|
208
224
|
working: Boolean(opts.working),
|
|
209
225
|
state,
|
|
210
226
|
ageMs,
|