English | 简体中文
agy-hud is a compact Antigravity CLI status-line HUD plugin written in TypeScript for Node.js.
It reads Antigravity status-line JSON from stdin and renders a short terminal HUD:
- Antigravity CLI 1.1.0 or newer. Install, status-line wiring and rendering are verified through 1.2.11; the loopback quota probe is verified through 1.1.26 and is refused from 1.2.2 on, where quota refreshes run the official
agy -p /usagein the background instead (see Quota Cache). The status line is wired with the CLI's native/statuslinecommand, which 0.1.8 relies on: thecomponentshook that olderplugin.jsonfiles declared is not honored by 1.1.x, so it has been dropped. On a 1.0.x CLI that predates/statuslinethere is no way to activate this version — stay on 0.1.7 or update the CLI. - Node.js 18+ available on
PATH - macOS or Linux. Windows is not currently supported because the plugin hook/install flow has not been verified there.
- A terminal font that carries Nerd Font glyphs, if you want the icons. Without one the four HUD icons render as boxes or
[?], which looks like a broken plugin but is not — see Icons Render As Boxes. Setting"show_icons": falsegives a plain-text HUD that needs no font at all.
agy-hud is distributed as an Antigravity plugin archive, not as an npm package. The archive includes the bundled runtime script at dist/agy-hud.js, so plugin users do not need to run npm install.
Download the platform-independent archive from the latest release:
curl -fsSL -o agy-hud.tar.gz \
https://github.com/franksde/agy-hud/releases/latest/download/agy-hud.tar.gz
tar -xzf agy-hud.tar.gz
agy plugin install ./agy-hudThe extracted directory is a complete plugin (plugin.json, hooks/, dist/), so it can be passed straight to agy plugin install. It runs with Node.js 18+ and does not require npm install after extraction.
Important: After installation, you must run the following slash command inside your Antigravity CLI to enable the HUD:
/statusline ~/.gemini/config/plugins/agy-hud/hooks/status-line.sh
Do not pass a git clone straight to agy plugin install. The command copies the entire directory, so it would drag .git/, src/, test/, and everything else into the plugin directory — and if the clone has git's fsmonitor enabled, the copy aborts outright on .git/fsmonitor--daemon.ipc, because it is a socket rather than a file.
Stage the same file set the release archive ships, then install that:
npm ci && npm run build && npm test
stage=$(mktemp -d)/agy-hud
mkdir -p "$stage/hooks" "$stage/dist" "$stage/docs"
cp plugin.json config.example.json README.md README.zh-CN.md LICENSE THIRD_PARTY_NOTICES.md SECURITY.md CONTRIBUTING.md CHANGELOG.md "$stage/"
cp hooks/status-line.sh "$stage/hooks/"
cp dist/agy-hud.js "$stage/dist/"
cp docs/hud-preview.png "$stage/docs/"
agy plugin validate "$stage"
agy plugin install "$stage"A local install lands in the same plugin directory as a release install, so the /statusline step above is required here too.
Installing a new version is not the same as upgrading. The CLI's statusLine.command points at a specific file, and agy plugin install does not update it. If your command still points at an older copy of dist/agy-hud.js, that older copy keeps running: the HUD looks fine, nothing errors, and you are simply not on the new version.
Start by finding out what you are actually running:
grep -A2 '"statusLine"' ~/.gemini/antigravity-cli/settings.jsonThe command is one of these two shapes. Either way, the plugin root is the agy-hud directory in that path — not the hooks directory the command names, and not dist:
/Users/you/.gemini/config/plugins/agy-hud/hooks/status-line.sh
^^^^^^^ plugin root
node /Users/you/.gemini/config/plugins/agy-hud/dist/agy-hud.js statusline
^^^^^^^ plugin root
Confirm the version that root is on:
node <plugin-root>/dist/agy-hud.js versionIf the HUD is currently working, the smallest upgrade is to overwrite the bundle inside that root. The wiring does not change and no slash command is needed:
curl -fsSL -o agy-hud.tar.gz \
https://github.com/franksde/agy-hud/releases/latest/download/agy-hud.tar.gz
tar -xzf agy-hud.tar.gz
cp agy-hud/dist/agy-hud.js <plugin-root>/dist/agy-hud.js
node <plugin-root>/dist/agy-hud.js version # confirm it now reports the new versionIf your HUD disappeared after updating the Antigravity CLI to 1.1.x, you were relying on the old components hook, which the CLI no longer registers. Install the new version normally and wire it up once with /statusline, as in the install sections above. This step is unavoidable: a plugin has no install hook that can write the status-line setting for you — components was that mechanism, and it is gone.
Reinstalling deletes a config.json kept inside the plugin directory. Since Antigravity CLI 1.1.28, agy plugin install replaces the plugin's directory exactly, so a config next to the bundle or in the plugin root does not survive it. Move it to ~/.config/agy-hud/config.json before reinstalling; doctor warns when the config in effect sits in that position. Overwriting only the bundle, as above, leaves it alone.
The quota cache needs no action either way. See Quota Cache.
The HUD draws four icons from the Nerd Font Private Use Area: model (U+EE9C), plan (U+F0A3), git branch (U+E725), and directory (U+F07C). A terminal font without those glyphs draws each one as a box or [?]. The quota reset arrow ↻ is plain Unicode (U+21BB), so it keeps rendering — a status line like [?] 3.8 Flash High | [?] Pro | [?] dev with a working ↻ is exactly this, and not a broken install.
Run the built-in check:
node <plugin-root>/dist/agy-hud.js doctorIt prints the resolved config, the terminal it detected, a font scan, and an icon probe. The probe is the part that answers the question, because nothing else can: a terminal never reports back which glyphs it actually drew, so no program can tell whether your screen is showing icons or boxes. Read the probe line yourself.
If the probe shows boxes, pick one.
A. Turn the icons off. Works everywhere, takes effect on the next redraw, needs no font:
mkdir -p ~/.config/agy-hud
echo '{"show_icons": false}' > ~/.config/agy-hud/config.json # merge the key if the file already existsThe HUD then renders as plain text: 3.8 Flash High | Pro | dev.
Check the config line in doctor output first. If it already names a file, edit that one instead: a config.json next to the bundle or in the plugin root outranks ~/.config/agy-hud/config.json (see Config for the full order), so a new file under ~/.config would sit there shadowed while nothing on screen changes.
B. Keep the icons. Two steps, and only the second one changes what you see:
- Install a Nerd Font, for example
brew install --cask font-hack-nerd-font. - Point the terminal at that family — iTerm2 → Settings → Profiles → Text → Font, VS Code's
terminal.integrated.fontFamily,font-familyin~/.config/ghostty/config, and so on.doctorprints the location for the terminal it detects.
Installing the font alone changes nothing, because the terminal keeps drawing with whatever family its own profile names.
Two limits on the font scan, which is why doctor reports it as a heuristic and never as a verdict. An installed Nerd Font is not necessarily the one your terminal draws with. And Ghostty, WezTerm and kitty ship their own Nerd Font symbol fallback, so a scan that finds nothing can still be a terminal that renders every icon correctly.
Over SSH the glyphs are drawn by the terminal on your local machine, so a font installed on the remote host cannot change them. Take option B on the local machine, or option A on the remote one.
The /statusline command stores its configuration separately from the plugin files. Clear that
configuration before uninstalling so the CLI does not retain a command that points at the removed
plugin:
/statusline delete
Then uninstall the plugin:
agy plugin uninstall agy-hudIf you are an AI agent installing agy-hud for someone, do not install silently, and do not assume the icons will render. Icons showing up as boxes is the most common complaint after an agent-run install, and it is a font condition on the user's machine that you cannot observe from a text stream.
First check whether they already have it, by reading statusLine.command in ~/.gemini/antigravity-cli/settings.json. If it already points at an agy-hud.js, this is an upgrade, not an install: follow Upgrading and overwrite that exact file. Do not agy plugin install into the new plugin directory and stop there — the CLI would keep running the old copy, the HUD would look perfectly healthy, and the user would never learn they are still on the old version.
For a genuinely new install:
-
Send the user this preview of a fully-populated HUD, so they see every element before anything is installed:
3.5 Flash High | Pro │ agy-hud │ main Context ░░░░░░░░░░ 0% │ Usage ████████░░ 82% (↻ 1h 52m) | █░░░░░░░░░ 13% (↻ 4d 21h)Tell them what they are looking at: model and plan tier, working directory, git branch, a context bar with token detail (
context_value: both), quota remaining as a continuous bar, local reset time, and agent state. The default config shows a more compactpercentcontext, and the HUD degrades cleanly on narrow terminals. -
In the same message, send the icon probe and ask about it directly:
model plan branch folderDoes each of those four labels have its own distinct icon in front of it, or do you see boxes,
[?], or blanks?Wait for the answer. You cannot answer this one yourself: you are reading a character stream, and the question is about what their font draws. The probe also only counts when your output lands in the same terminal they run the Antigravity CLI in. If they are reading you through a web UI or a different terminal, say so and have them run
doctorthemselves after step 4 — it prints the same probe inside their own terminal, which is the only place the answer means anything. -
If they report boxes, write the config before installing, so the first HUD they ever see is a working one:
mkdir -p ~/.config/agy-hud echo '{"show_icons": false}' > ~/.config/agy-hud/config.json
If the file already exists, merge the key in — read it, set
show_icons, write it back. Do not overwrite a config the user has already tuned. And ifdoctorreports aconfigPathother than this one, write to that path instead: a config next to the bundle outranks the home one, and a file written here would be shadowed.Do not install a font on their behalf unless they ask for it. It is a system-level change, and on its own it fixes nothing: the terminal keeps drawing with the family named in its own profile, and that setting is not reliably scriptable. Over SSH it is worse than useless, because the glyphs are drawn by the terminal on their local machine and a font installed on the remote host cannot change them. If they do want icons, give them the two steps from Icons Render As Boxes and let them do the second one.
-
Ask whether they want to adjust
multiline,context_value, orusage_value(see Config). Then downloadhttps://github.com/franksde/agy-hud/releases/latest/download/agy-hud.tar.gz, extract it, and runagy plugin install <extracted-dir>. Pass the extracted archive directory, never a git clone (see Install From Local Path for why). If it fails, show the user the error instead of retrying silently. -
Verify the install with
node <plugin-root>/dist/agy-hud.js doctor --json, which reports it as machine-readable JSON:{ "version": "0.1.11", "homedir": "/home/u", "nodeVersion": "v22.14.0", "nodeOk": true, "statuslineCommand": "/home/u/.gemini/config/plugins/agy-hud/hooks/status-line.sh", "statuslineWired": true, "configPath": "/home/u/.config/agy-hud/config.json", "suggestedConfigPath": "/home/u/.config/agy-hud/config.json", "userConfigPath": "/home/u/.config/agy-hud/config.json", "configInPluginDir": false, "showIcons": false, "terminal": "iTerm.app", "remoteSession": false, "nerdFont": "not-found", "nerdFontMatches": [] }Act on it as follows:
nodeOk: false— the runtime is older than 18. Report it; the HUD will not run.statuslineWired: false— the CLI is not running this plugin. Tell the user to run/statusline <plugin-root>/hooks/status-line.sh, because the HUD does not appear until they do. AstatuslineCommandthat is set but not wired means something else owns their status line; say what it is rather than overwriting it.showIcons— confirms which mode step 3 actually landed in.configInPluginDir: true— the config in effect lives inside the plugin directory, and the nextagy plugin installon Antigravity CLI 1.1.28+ deletes it. Tell the user, and with their agreement move it touserConfigPath: merge it into any file already there, then remove the plugin-directory copy, which would otherwise keep outranking it until the reinstall silently drops it.nerdFont— a hint, never a decision.not-founddoes not mean the icons are broken, because Ghostty, WezTerm and kitty ship their own glyph fallback.founddoes not mean they work either, because the terminal may draw with a different family. Never turn icons off, and never propose a font install, on the strength of this field alone. The user's answer in step 2, or the probe printed by plaindoctor, is the only evidence that settles it.
Plain
doctor, without--json, prints the same report plus the icon probe, so it also serves as the step 2 check for a user who was reading you somewhere other than their CLI terminal.
GitHub releases are expected to publish one platform-independent archive:
agy-hud.tar.gz
The archive should contain plugin.json, hooks/status-line.sh, dist/agy-hud.js, config.example.json, README.md, README.zh-CN.md, LICENSE, THIRD_PARTY_NOTICES.md, and supporting docs. Runtime dependencies are bundled, so no extra installation is required.
Installing the plugin does not put an agy-hud command on your PATH — the archive ships a bundle, not an npm package. Run the bundle with node, using the plugin root that your statusLine.command points into (~/.gemini/config/plugins/agy-hud for a current install):
node <plugin-root>/dist/agy-hud.js statusline < statusline_payload.json
node <plugin-root>/dist/agy-hud.js version
node <plugin-root>/dist/agy-hud.js quota refresh
node <plugin-root>/dist/agy-hud.js doctor
node <plugin-root>/dist/agy-hud.js doctor --jsonThe examples below use agy-hud as shorthand for that. If you use these often, alias it.
doctor self-checks an install: Node version, which command the CLI has wired to its status line, which config file is in effect and what show_icons resolves to, the terminal it detects, whether this is an SSH session, a heuristic font scan, and an icon probe for you to read. It changes nothing on disk. --json prints the same report as machine-readable JSON, for an agent that needs to check an install it just performed; see For AI Agents for how to act on each field.
statusline renders from stdin plus local config/cache files. When agent_state settles from active work back to idle, it performs one local loopback quota refresh before rendering so the same redraw can reflect post-response quota. Missing or stale cache data can still refresh in the background as a fallback. quota refresh asks the running Antigravity local server for GetUserStatus, writes the sanitized quota cache, and exits non-zero if no local server can be reached. On Antigravity CLI 1.2.2+, where that server refuses the request, the same-frame refresh is skipped and quota refresh falls back to the official agy -p /usage, run only in the background; see Quota Cache.
agy-hud looks for config in:
AGY_HUD_CONFIGAGY_HUD_GIT_BRANCHfor an explicit git branch display overrideconfig.jsonnext to the bundled script or plugin root$XDG_CONFIG_HOME/agy-hud/config.json$HOME/.config/agy-hud/config.json
Prefer the user-level path. A config.json inside the plugin directory still works, but Antigravity CLI 1.1.28+ replaces that directory exactly on agy plugin install, so the next reinstall deletes it without a warning. doctor flags a config in that position.
Default config:
{
"show_model": true,
"show_progress_bar": true,
"multiline": true,
"color": true,
"debug": false,
"show_git_branch": true,
"show_cwd": true,
"show_agent_state": true,
"show_cost": true,
"show_title": false,
"show_icons": true,
"context_value": "percent",
"usage_value": "remaining"
}show_progress_bar and multiline default to true, matching the preferred compact two-line HUD. debug defaults to false; keep it disabled for normal use so status-line output stays clean. AGY_HUD_GIT_BRANCH is intended for environments where Antigravity does not provide a branch and the hook process cannot resolve one from the workspace.
When workspace paths are available, git branch display is resolved from the current workspace/worktree before falling back to Antigravity's VCS branch payload.
Display options:
show_agent_state: shows stdinagent_statesuch asIdle,Thinking, orAuth.show_cost: shows the session cost supplied ascost.total_usdby Antigravity CLI 1.1.21+, at the end of the top line (or the single line).estimated: trueadds~, for example~$0.02. Zero displays as$0.00, positive amounts below $0.001 as<$0.001; missing, negative or non-finite values are omitted. The HUD does not separately addsubagent_usdor calculate subscription charges; this is not an invoice or a promise of actual billing.show_title: shows the conversation title supplied asconversation_titleby Antigravity CLI 1.1.27+, between the branch and the agent state (before the state on the single line). Off by default. It is reduced to one plain line with control characters removed, clipped to 24 columns, and is the first segment dropped when the line does not fit, before cost.show_icons: shows Nerd Font icons. Set tofalseto fall back to plain text if your terminal font renders boxes. Rundoctorto see the icon probe and the exact fix; Icons Render As Boxes covers the whole question.context_value:percent,tokens, orboth. Default ispercent, so context shows current input-side window occupancy. When token totals are available, the percentage and bar are derived fromtotal_input_tokens / context_window_sizeso a large latest response does not make the HUD jump.usage_value:remainingorpercent. Default isremaining, so quota text and bar show what is left. When Antigravity provides both windows, the HUD shows them separately with per-window reset durations, for exampleUsage ████████░░ 82% (↻ 1h 52m) | █░░░░░░░░░ 13% (↻ 4d 21h).
Cost is hidden before any existing segment when space is tight; the remaining layout then follows the usual degradation order. CJK, emoji and combining marks are measured in terminal columns, and clipping does not split grapheme clusters. Exact emoji appearance still depends on your terminal and font.
Plan badges recognize Pro, Ultra, Free and their Google AI-prefixed forms. Unknown or missing tiers show Plan ?, not Free.
On Antigravity CLI 1.0.8 and newer, agy-hud reads the official quota object from the status-line payload first. If the payload includes both 5-hour and weekly windows, the HUD renders both in order instead of collapsing them into one ambiguous number. If an official bucket still looks untouched while a fresh active-model cache already shows consumption, agy-hud uses the fresh cache to avoid showing a stale 100% left. Older CLI versions, or payloads without official quota data, fall back to the local quota cache. The default cache path is:
$XDG_CACHE_HOME/agy-hud/quota_cache.json
$HOME/.cache/agy-hud/quota_cache.json # when XDG_CACHE_HOME is unset
You can override it with AGY_HUD_QUOTA_CACHE, which then becomes the only path used for both reads
and writes.
Before 0.1.8 the cache lived at $HOME/.gemini/antigravity-cli/scratch/agy-hud/quota_cache.json,
inside a directory the Antigravity CLI abandoned in 1.1.0. Upgrading needs no action: the HUD still
reads that old file when no new cache exists yet, and the first refresh writes the new one. The old
file is left in place, so downgrading keeps working too.
If the new cache exists but cannot be parsed — a write interrupted by a crash, say — the HUD renders from the old cache and forces a refresh to rewrite the damaged file, rather than leaving it masked.
Refresh the fallback cache manually when Antigravity is running:
node <plugin-root>/dist/agy-hud.js quota refreshThe refresh command supports both known Antigravity local-server shapes: the current agy loopback server and the older language_server --csrf_token ... process, in that order. If a CSRF token is present, it is used only for the loopback GetUserStatus request. The command stores only the sanitized cache shape below. Normal statusline rendering reads this cache and refreshes it when active work settles. It also uses stale-cache refreshes as a fallback. If the cache still looks untouched (100% left for every model), status-line activity such as a new conversation or agent state change can trigger an immediate debounced background refresh.
Since 0.1.9, quota refreshes can reuse quota_cache.json.server.json next to the quota cache (or <AGY_HUD_QUOTA_CACHE>.server.json). It holds only a PID, local port, process identity (start time and executable path), and discovery timestamp. Each reuse checks the process identity with a targeted ps call, avoiding a full process scan and lsof; hints expire after five minutes. Failure or malformed quota causes discovery in the same refresh. Legacy servers requiring CSRF are never cached in this hint. Quota refresh intervals, background refreshes and working-to-idle same-frame correction are unchanged.
From Antigravity CLI 1.2.2 on (boundary measured by CodexBar; verified here on 1.2.11), the agy loopback server answers GetUserStatus with 401 missing CSRF token, and the CLI does not give the status-line command that token. The status-line payload still carries the official quota, but it lags: during a turn it does not move at all, and a turn's usage booked after the CLI's end-of-turn fetch stays invisible until the next turn ends. Measured on 1.2.11, an idle session showed a 5h value 25 minutes out of date.
On such a CLI, quota refresh falls back to the official read-only command agy -p /usage --output-format json. It starts no agent turn and spends no tokens or quota, but it does start a separate, short-lived agy process: about 7 s, most of it waiting on Google's API, about 1 s of CPU and a transient 160 MB. The HUD never runs it during a redraw, only in the background, and paces it:
| When | Runs if the cache is older than |
|---|---|
| A turn is running, or has just settled | 60 s |
| The CLI sits idle (a redraw from typing, for example) | 5 minutes |
Only one run happens at a time across every session: a lock next to the cache names its owner, only that owner runs /usage and removes the lock, and one older than 2 minutes is taken over. A failing run backs off for 60 s, doubling to 10 minutes. The agy it starts renders the status line too; agy-hud marks that child with AGY_HUD_NESTED=1, and a status line carrying the mark only renders.
The refusal is recorded in quota_cache.json.auth-rejected.json (or <AGY_HUD_QUOTA_CACHE>.auth-rejected.json) together with the CLI version from the status-line payload, plus the backoff. While the payload reports that version, refreshes go straight to /usage. After a CLI update the next refresh tries loopback again, and a loopback success deletes the file. A payload without a version never counts as refused, and a manual quota refresh always tries loopback first. Any other loopback failure, such as no running server, never falls back to /usage, so older CLIs behave exactly as before.
A /usage cache holds the same buckets as the payload's quota field. For each window, 5h and weekly, the HUD shows the fresher of the two readings. A reading whose reset_time has passed belongs to an ended window: a cached one is ignored, and a payload one yields to the cache. Two live readings are the same window, and quota only goes down within a window, so the lower remainder is the newer reading. (An untouched bucket's reset_time moves with each query, so it cannot order readings.)
Expected sanitized cache shape:
{
"timestamp": "2026-05-19T12:00:00Z",
"plan_name": "Pro",
"models": {
"Gemini 3.5 Flash (Medium)": {
"remainingFraction": 0.2,
"resetTime": "2026-05-19T12:44:00Z"
}
}
}A /usage refresh writes the payload's bucket shape instead:
{
"timestamp": "2026-09-25T14:20:28Z",
"source": "usage",
"models": {},
"quota": {
"gemini-5h": { "remaining_fraction": 0.867, "reset_time": "2026-09-25T14:51:03Z" },
"gemini-weekly": { "remaining_fraction": 0.853, "reset_time": "2026-09-30T06:16:21Z" }
}
}If quota data is missing, the HUD omits the usage segment instead of showing a fake limit. Official quota payloads can include live reset_in_seconds, so dual-window quota displays show per-window relative reset durations. The local fallback cache still derives reset from the local API's resetTime field and displays it as a local clock time.
agy-hud statusline renders from stdin plus local optional config/cache files. It does not transmit status-line payload data externally. Quota refreshes contact only the local Antigravity loopback server, or, where the CLI refuses that, run the official agy -p /usage command, which talks to Google with your own sign-in exactly as the CLI always does. agy-hud itself makes no network request.
agy-hud quota refresh contacts only the local Antigravity server on loopback, or runs agy -p /usage, and does not print CSRF tokens, cookies, raw probe responses, or /usage error text.
The renderer intentionally avoids printing sensitive status-line fields, including email, session IDs, conversation IDs, transcript paths, tokens, CSRF values, cookies, keys, and full workspace paths. Git branch detection reads .git/HEAD directly and does not run git.
agy-hud doctor reads only local files and prints only local facts; it sends nothing anywhere and writes nothing. Because its output is what you would paste into a bug report, paths under your home directory are abbreviated to ~/…, which drops your account name. doctor --json keeps them absolute, since it is meant for an agent acting on the same machine — review that output before pasting it anywhere.
Do not put raw Antigravity probe payloads, logs, cookies, tokens, emails, or local machine paths in issues or pull requests.
npm ci
npm run build
npm testnpm run build bundles src/main.ts into dist/agy-hud.js. Commit the updated dist/agy-hud.js with any source changes so cloned plugins can run without a build step.
Quota fields depend on local Antigravity availability and a compatible local cache. If Antigravity is not running, or its local GetUserStatus endpoint changes, the HUD omits quota details.
