statusLine updates are capped at 1 Hz — smooth animation is impossible
What I want
A status line that can update at a high frame rate — smooth motion, not discrete
state flips. Ideally something in the neighbourhood of the display's refresh
rate, opt-in, for authors who want to pay for it.
Right now it's capped at 1 Hz, and the cap is hard-coded in two places.
The cap
Checked against the 2.1.217 binary. The setting is validated with a min(1):
refreshInterval: v.number().min(1).optional().catch(void 0)
.describe("Re-run the status line command every N seconds in addition to event-driven updates")
and clamped again where the interval hook is installed:
let G = u?.refreshInterval;
gc(U, G !== void 0 ? Math.max(1, G) * 1000 : null);
Measured
I set refreshInterval: 0.2, wrapped my status line in a script that logged a
millisecond timestamp on every invocation, and collected 157 ticks across idle
and active periods:
min 0.300s median 1.000s max 1.281s
0.2–0.3s : 1
0.3–0.4s : 4
0.4–0.5s : 3
0.5–0.6s : 3
0.6–0.7s : 6
0.7–0.8s : 3
0.9–1.0s : 47 ← the timer
1.0–1.1s : 86 ← the timer
1.2–1.3s : 3
The 133 ticks in the 0.9–1.1 s buckets are the timer, pinned at 1.000 s no matter
what I asked for.
The 20 ticks under 0.8 s are event-driven re-renders, which fire whenever
session state changes and are debounced at 300 ms — the fastest observed gap is
exactly 0.300 s, the debounce floor. Those only happen while the model is
actively working.
So the harness is already comfortable re-running the command 2–3×/second during
activity. The 1 Hz cap only binds when the session is idle, which is exactly when
a status line has the most room to show something interesting.
Why 1 Hz specifically doesn't work
At one frame per second, any continuous motion — a sweep, a pulse, a progress
shimmer — reads as broken rendering rather than as animation. The frames are too
far apart for the eye to connect them. In practice status-line authors are
limited to discrete on/off state flips.
I worked around it by animating only while the model is working (detected by
watching total_api_duration_ms advance), since that's when the event-driven
path lifts the effective rate to 2–3 fps, and rendering byte-identical output
when idle. It works, but it means the one thing that determines whether animation
is viable is a side effect I have to infer.
Two smaller notes
refreshInterval: 0.2is silently rounded up, not rejected.Math.max(1, 0.2)
wins, so the setting looks accepted and does nothing. A warning or a doc note
would save people the experiment.
- Output is only taken on process exit (
let l = await r(); if (t.aborted) return; if (i(l), l) ...),
and an in-flight run is discarded when a new update supersedes it. That's a
reasonable design, but it isn't stated in the status-line docs, and it rules
out the workaround people will reach for first.
For reference
Per-frame cost matters here — measured on an M-series Mac, 500 iterations each:
| what it runs | cost per invocation |
| --- | --- |
| /bin/sh + echo hi | 3.29 ms |
| #!/usr/bin/env bash + echo hi | 6.43 ms |
| my real status line (unchanged tick, no jq/git) | 20.90 ms |
I'm not going to tell you how to build it — you'll know the constraints better
than I do. But I'd love to be able to opt into a genuinely high refresh rate.
Environment
- Claude Code 2.1.217, macOS (darwin arm64), Apple Silicon
- Reproduced with a plain
bashstatus-line script