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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "usage-limits",
3
3
  "displayName": "Usage Limits",
4
- "version": "1.11.5",
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.5",
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
- 477 tests over the pricing, the window arithmetic, plan and credit detection,
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.5",
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
- return (
170
- paint(fillGlyph.repeat(filled), levelColour(opts.level || level(pct)), mode) +
171
- paint(emptyGlyph.repeat(cells - filled), THEME.empty, mode)
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
- case 'max':
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
- ultracode: Boolean(
625
- hookInput && typeof hookInput.prompt === 'string' && /\bultracode\b/i.test(hookInput.prompt)
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
- if (!usage.isCodex()) {
639
- try {
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
- const found = (options && options.meter) || meterFromDisk();
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) return { working: false, ultracode: false };
90
- return { working: mine.state === 'working', ultracode: Boolean(mine.ultracode) };
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
- ? built.ultracode
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
- // Ultracode is xhigh plus workflows, and Claude names it as its own level in
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
- ultracode: own.ultracode || settings.ultracode === true,
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
- const slot = feed.newest(slots);
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
- effort: slot ? slot.effort : (onCodex && collected.settings && collected.settings.effortLevel !== 'default' ? collected.settings.effortLevel : null),
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
- ultracode: seen.ultracode || settings.ultracode === true,
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
- ? built.ultracode
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 = built.ultracode
363
- ? bars.rainbow(titleText, tick, { mode, reduced })
364
- : animate
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.ultracode ? 'ultracode' : built.effort;
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 who = [built.modelLabel, effortText, bars.dim(status, mode)].filter(Boolean).join(bars.dim(' · ', mode));
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 ? bars.paint((ascii ? '-' : '░').repeat(barWidth), bars.THEME.empty, mode) : bars.bar(row.percent, barWidth, { mode, level: row.level, ascii })) +
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
- if (!usage.isCodex()) {
138
- try {
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
- const ultracode = Boolean(opts.ultracode) || effort === 'max' || effort === 'ultracode';
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,