This guide explains how to set up a comprehensive custom status line for the Antigravity (agy) CLI. The status line provides real-time information about your current session, including context window usage, daily quota limits, current workspace, git status, and the active model.
Installation: Simply tell Antigravity to install it for you! Just type this into your prompt:
install https://gist.github.com/it3xl/3d7b91d6b8fa3c790fdbcfc0e8d0321b
Preview:
20:48 my-rename Model ctx:8% (961k of 1M) d-21% 15:06 2h 17m w-14% Thu 14:27 5d 1h ~/my main +1 ~2 account subscription
What each item means:
20:48: Current time (updates whenever the prompt renders).my-rename: The name of your current conversation (only appears here if you've explicitly renamed your session by typing/rename my-rename).Model: Active AI model.ctx:8% (961k of 1M): Percentage of your context window currently used, with raw token counts.d-21% 15:06 2h 17m: Daily (5h) quota consumed for the active model class, local reset time, and time remaining.w-14% Thu 14:27 5d 1h: Weekly quota consumed, day and time of reset, and time remaining.~/my: Shortened current working directory.main +1 ~2: Git branch and file status (staged/modified files). If there are no uncommitted changes, it will show asclean. This block will disappear if the current folder is not a git repository.account: Active authenticated Google account.subscription: Your active subscription tier.
This refined approach handles the latest CLI features robustly by returning to a zero-polling Native-JSON architecture.
statusline.py: The core formatter. It reads the JSON output provided natively by the CLI onstdinand extracts context window usage, workspace path, model name, daily quota limits, and active Google account.- Zero-Polling: Utilizes native quota statistics supplied directly by the CLI JSON output. No background cache polling, no CPU overhead.
- Cross-Platform Support: Fully compatible with macOS, Windows, and Linux. No bash wrappers required.
- Git Status: Non-blocking fast git checks (timeout clamped to 1s to ensure your prompt never hangs in massive repos).
- Configuration Automation: Setup is a single settings edit.
Note
Important Quota Delay Warning: Because we read directly from the native stdin payload to maximize performance, the quota depends entirely on Google's Language Server. After an overnight sleep or long period of inactivity, it can take a few minutes and multiple prompts before Google's backend fully synchronizes your fresh quota with the local CLI. During this time, the status line will display diagnostic codes indicating exactly why the data is delayed (most commonly d-?, meaning the local cache has expired and is waiting for Google to send fresh data).
If you are the AI agent reading this Gist to fulfill an installation request, you MUST execute the following deployment protocol:
- Prerequisite Check: Before doing anything, verify that Python 3 is installed on the user's system by running
python3 --versionorpython --version. If Python is missing, you MUST abort the installation and politely instruct the user to install Python 3 first to let the custom status line work. - Analyze & Adapt: Review the provided
statusline.pyscript below. Check your current knowledge for any recent changes to the Antigravity API payload structure (the stdin JSON passed to the status hook) and proactively adapt the Python script to accommodate them. - Pre-flight Testing: To guarantee it functions flawlessly on the user's specific operating system and environment, you must test it before installing. Create a temporary Python script in the OS temp folder, feed it a mock JSON payload representing the current
agystatus API, and execute it to verify the output doesn't crash or error. - Diagnostic Correction: If your pre-flight testing unexpectedly outputs any Quota Diagnostic Codes (e.g.,
d-noquota,d-?model,d-err,d-?format), do not ignore them. You must intervene and adapt the Python script logic to handle the user's specific model tier, or properly parse the new JSON payload structure, before proceeding. - Install: As a highly capable agent, adjust the script for the user's OS flavor as needed. Once verified, save the working script to
~/.gemini/statusline.pyand configure~/.gemini/antigravity-cli/settings.jsonto enable it. (Be sure to cleanly remove any oldfetch_quota.pyarchitecture if present). - Confirmation: After completing the setup, tell the user that the custom status line is successfully installed. Based on your knowledge of the current
agyCLI version, advise the user on how to make it appear (for example, they may need to hitEnterto refresh the prompt, or run the/statuslinecommand to toggle/enable it). Finally, ask them to report back if it still doesn't show up so you can troubleshoot.
If your quota data is missing or delayed, the status line will output specific diagnostic codes so you know exactly why (Note: The codes for the w- weekly quota are identical):
d-noquota= No quota JSON object at all in the payload.d-?model= Quota object exists, but no data available for your specific model tier.d-syncing= Data exists, but the reset time is in the past (morning wake-up / stale data waiting to refresh).d-??% <time>= Time exists, but the usage percentage fraction is missing.d-<pct>% ??:??= Percentage exists, but the reset time is missing.d-<pct>% ?format= The time string is formatted incorrectly (cannot parse the clock).d-err= A Python math exception occurred while calculating the percentage from the payload.
Create a file at ~/.gemini/statusline.py and paste the following Python script:
import json, sys, datetime, os, subprocess, urllib.parse
# Source: https://gist.github.com/it3xl/3d7b91d6b8fa3c790fdbcfc0e8d0321b
# Force UTF-8 on stdin/stdout for Windows compatibility
for _stream in (sys.stdin, sys.stdout):
try:
_stream.reconfigure(encoding='utf-8', errors='replace')
except Exception:
pass
def fmt_num(n):
n = int(n)
if n >= 1_000_000:
return f"{n / 1_000_000:.1f}".replace('.0', '') + "M"
if n >= 1000:
return f"{int(n / 1000)}k"
return str(n)
# Colors
R = '�[0m'
G = '�[32m' # green - user
C = '�[36m' # cyan - model, branch, plan tier
DC = '�[38;5;66m' # dim cyan - git status
Y = '�[33m' # yellow - ctx
M = '�[35m' # magenta - path
TM = '�[97m' # bright white - time
LD = '�[91m' # bright red - day limit pct
DLD = '�[38;5;174m' # muted red - day reset time
LW = '�[38;5;220m' # gold yellow - week limit pct
DLW = '�[38;5;179m' # dim yellow - week reset time
try:
data = json.load(sys.stdin)
except Exception:
data = {}
# Model
model = (data.get('model') or {}).get('display_name') or data.get('model_name')
if not model:
try:
settings_path = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity-cli', 'settings.json')
with open(settings_path, 'r', encoding='utf-8') as f:
model = json.load(f).get('model')
except Exception:
pass
if not model:
model = os.environ.get('AGY_MODEL', '?model')
model_lower = model.lower()
# Directory
ws = data.get('workspace') or {}
cwd = ws.get('project_dir') or data.get('cwd') or ws.get('current_dir') or os.getcwd()
if cwd.startswith('file://'):
cwd = urllib.parse.unquote(cwd[7:])
if os.name == 'nt' and cwd.startswith('/'):
cwd = cwd[1:]
# Context window
ctx_obj = data.get('context_window') or {}
used_pct = ctx_obj.get('used_percentage')
ctx_size = ctx_obj.get('context_window_size')
remaining_pct = ctx_obj.get('remaining_percentage')
if used_pct is not None:
ctx_str = f'ctx:{round(used_pct)}%'
if remaining_pct is not None and ctx_size:
left = int(ctx_size * remaining_pct / 100)
ctx_str += f' ({fmt_num(left)} of {fmt_num(ctx_size)})'
else:
ctx_str = 'ctx:?'
# --- QUOTA DIAGNOSTIC LOGIC ---
fh_used = None
fh_reset_str = None
quota_status = 'ok'
pct_text = None
wk_used = None
wk_reset_str = None
wk_status = 'ok'
wk_pct_text = None
quota_obj = data.get('quota')
if quota_obj is None:
quota_status = 'missing_obj' # d-noquota
wk_status = 'missing_obj' # w-noquota
else:
is_gemini = 'gemini' in model_lower
key_5h = 'gemini-5h' if is_gemini else '3p-5h'
key_wk = 'gemini-weekly' if is_gemini else '3p-weekly'
q = quota_obj.get(key_5h)
qw = quota_obj.get(key_wk)
if not q:
quota_status = 'missing_model' # d-?model
else:
if 'remaining_fraction' in q:
rem_frac = q['remaining_fraction']
try:
fh_used = (1.0 - float(rem_frac)) * 100.0
pct_text = f'd-{round(fh_used)}%'
except (ValueError, TypeError):
quota_status = 'math_err' # d-err
pct_text = 'd-err'
else:
quota_status = 'missing_fraction' # d-??%
pct_text = 'd-??%'
fh_reset_str = q.get('reset_time')
if not fh_reset_str:
if quota_status == 'ok':
quota_status = 'missing_time'
if not qw:
wk_status = 'missing_model' # w-?model
else:
if 'remaining_fraction' in qw:
wk_rem_frac = qw['remaining_fraction']
try:
wk_used = (1.0 - float(wk_rem_frac)) * 100.0
wk_pct_text = f'w-{round(wk_used)}%'
except (ValueError, TypeError):
wk_status = 'math_err' # w-err
wk_pct_text = 'w-err'
else:
wk_status = 'missing_fraction' # w-??%
wk_pct_text = 'w-??%'
wk_reset_str = qw.get('reset_time')
if not wk_reset_str:
if wk_status == 'ok':
wk_status = 'missing_time'
def fmt_day_limit(status, pct_txt, reset_str):
if status == 'missing_obj':
return f'{LD}d-noquota{R}'
if status == 'missing_model':
return f'{LD}d-?model{R}'
if status == 'math_err':
return f'{LD}d-err{R}'
if status == 'missing_time':
return f'{LD}{pct_txt} ??:??{R}'
if not reset_str:
return f'{LD}{pct_txt}{R}'
try:
reset_str_fixed = reset_str.replace('Z', '+00:00')
reset_dt = datetime.datetime.fromisoformat(reset_str_fixed).astimezone()
now = datetime.datetime.now(datetime.timezone.utc)
diff = reset_dt - now
if diff.total_seconds() > 0:
h, m = divmod(int(diff.total_seconds() // 60), 60)
dur = f'{h}h {m}m' if h else f'{m}m'
return f'{LD}{pct_txt}{R} {DLD}{reset_dt.strftime("%H:%M")}{R} {LD}{dur}{R}'
else:
return f'{LD}d-syncing{R}' # Time is in the past!
except Exception:
return f'{LD}{pct_txt} ?format{R}'
def fmt_week_limit(status, pct_txt, reset_str):
if status == 'missing_obj':
return f'{LW}w-noquota{R}'
if status == 'missing_model':
return f'{LW}w-?model{R}'
if status == 'math_err':
return f'{LW}w-err{R}'
if status == 'missing_time':
return f'{LW}{pct_txt} ??:??{R}'
if not reset_str:
return f'{LW}{pct_txt}{R}'
try:
reset_str_fixed = reset_str.replace('Z', '+00:00')
reset_dt = datetime.datetime.fromisoformat(reset_str_fixed).astimezone()
now = datetime.datetime.now(datetime.timezone.utc)
diff = reset_dt - now
if diff.total_seconds() > 0:
d = diff.days
h, m = divmod(int(diff.seconds // 60), 60)
if d > 0:
dur = f'{d}d {h}h'
else:
dur = f'{h}h {m}m' if h else f'{m}m'
return f'{LW}{pct_txt}{R} {DLW}{reset_dt.strftime("%a %H:%M")}{R} {LW}{dur}{R}'
else:
return f'{LW}w-syncing{R}'
except Exception:
return f'{LW}{pct_txt} ?format{R}'
day_str = f" {fmt_day_limit(quota_status, pct_text, fh_reset_str)}"
week_str = f" {fmt_week_limit(wk_status, wk_pct_text, wk_reset_str)}"
# Git info (timeout clamped to 1s to prevent hanging prompts)
try:
branch = subprocess.check_output(
['git', '-C', cwd, 'branch', '--show-current'],
stderr=subprocess.DEVNULL, timeout=1
).decode('utf-8', 'replace').strip() or '?'
status_out = subprocess.check_output(
['git', '-C', cwd, 'status', '--porcelain'],
stderr=subprocess.DEVNULL, timeout=1
).decode('utf-8', 'replace').strip()
if status_out:
lines = status_out.splitlines()
def conflict(l): return 'U' in l[:2] or l[:2] in ('AA', 'DD')
conflicted = sum(1 for l in lines if conflict(l))
staged = sum(1 for l in lines if not conflict(l) and l[0] in 'MADRCT')
modified = sum(1 for l in lines if not conflict(l) and l[1] in 'MDT')
untracked = sum(1 for l in lines if l.startswith('??'))
parts = []
if conflicted: parts.append(f'!{conflicted}')
if staged: parts.append(f'+{staged}')
if modified: parts.append(f'~{modified}')
if untracked: parts.append(f'?{untracked}')
git_status = ' '.join(parts) if parts else 'clean'
else:
git_status = 'clean'
except Exception:
branch = ''
git_status = ''
now_str = datetime.datetime.now().strftime('%H:%M')
account = data.get('email', '')
plan_tier = data.get('plan_tier', '')
home = os.path.expanduser('~')
if os.name == 'nt':
home = home.replace('\\', '/')
cwd = cwd.replace('\\', '/')
# Check directory boundary to avoid matching sibling folders (e.g. ~/ann vs ~/annette)
low_cwd, low_home = cwd.lower(), home.lower().rstrip('/')
if low_cwd == low_home:
display_cwd = '~'
elif low_cwd.startswith(low_home + '/'):
display_cwd = '~' + cwd[len(low_home):]
else:
display_cwd = cwd
conv_id = data.get('conversation_id')
conv_name = ""
if conv_id:
history_path = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity-cli', 'history.jsonl')
try:
if os.path.exists(history_path):
with open(history_path, 'r', encoding='utf-8') as f:
for line in f:
if conv_id in line and '/rename ' in line:
try:
item = json.loads(line)
if item.get('conversationId') == conv_id:
display = item.get('display', '')
if display.startswith('/rename '):
conv_name = display[8:].strip()
except Exception:
pass
except Exception:
pass
name_str = f' {M}{conv_name}{R}' if conv_name else ''
path_part = f'{M}{display_cwd}{R}'
branch_str = f' {C}{branch}{R}' if branch else ''
gs_str = (f' {DC}{git_status}{R}' if git_status and git_status != 'clean' else f' {DC}clean{R}') if branch else ''
account_str = f' {G}{account}{R}' if account else ''
plan_str = f' {C}{plan_tier}{R}' if plan_tier else ''
print(f'{TM}{now_str}{R}{name_str} {C}{model}{R} {Y}{ctx_str}{R}{day_str}{week_str} {path_part}{branch_str}{gs_str}{account_str}{plan_str}')You don't need any shell wrapper scripts! You can configure Antigravity to call the Python script directly.
Run the following single-line command in your terminal to automatically enable the status line in your Antigravity configuration:
jq '.statusLine = {"type": "custom", "command": "python3 ~/.gemini/statusline.py", "enabled": true}' ~/.gemini/antigravity-cli/settings.json > /tmp/agy_settings.json && mv /tmp/agy_settings.json ~/.gemini/antigravity-cli/settings.jsonOpen your settings file located at C:\Users\YourUsername\.gemini\antigravity-cli\settings.json and add/update the statusLine block:
"statusLine": {
"type": "custom",
"command": "python C:\\Users\\YourUsername\\.gemini\\statusline.py",
"enabled": true
}That's it! Type /statusline in your Antigravity prompt (or simply submit a message) and enjoy your new beautifully detailed status line.
Warning
Do not use an infinite while true loop here to auto-refresh the status line!
Unlike Claude Code (which supports long-running streaming status scripts), the Antigravity CLI enforces a strict process timeout for the custom status line hook. It expects the script to print and exit immediately. If you wrap this in an infinite bash loop, Antigravity will terminate the background process with a SIGKILL after a few seconds, resulting in a persistent Statusline error: command failed: signal: killed polluting your prompt. Therefore, the status line will only refresh when you actively interact with the CLI prompt.
Migration note: Do not manually copy the script to a new machine or OS login. APIs and formats change rapidly. Instead, always ask the agent to install or update the status line directly from this gist. The agent will pull the latest version and configure it for your current environment.