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 Gemini 3.1 Pro (High) ctx:8% (961k of 1M) d-21% 15:06 2h 17m ~/my main +1 ~2 my@email.com
What each item means:
20:48: Current time (updates whenever the prompt renders).my-session-name: The name of your current conversation (only appears here if you've explicitly renamed your session by typing/rename my-session-name).Gemini 3.1 Pro (High): Active AI model.ctx:8% (961k of 1M): Context window usage (percentage, tokens remaining, and total size).d-21%: Daily quota consumed for the active model class.15:06: Local time when your daily quota resets.2h 17m: Time remaining until the quota resets.~/my: Current workspace or working directory.main +1 ~2: Git branch and file status (staged/modified files). If there are no uncommitted changes, it will show asclean.my@email.com: Active authenticated Google account.
This refined approach handles the latest CLI features robustly:
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.- 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).
fetch_quota.py: A lightweight helper script that syncs the real quota from the language server HTTP port, solving the "stale quota on morning wakeup" issue.- Hybrid Syncing: The formatter intelligently runs this fetcher asynchronously in the background for snappy prompts, but switches to a fast synchronous fetch on wake-ups so you never see stale data!
- Configuration Automation: Setup is a single settings edit.
If you are the AI agent reading this Gist to fulfill an installation request, you MUST execute the following deployment protocol:
- Analyze & Adapt: Review the provided
statusline.pyandfetch_quota.pyscripts 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 scripts 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 temporary Python scripts 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. - Install: As a highly capable agent, adjust the script for the user's OS flavor as needed. Once verified, save the working scripts to
~/.gemini/statusline.pyand~/.gemini/fetch_quota.py, then configure~/.gemini/antigravity-cli/settings.jsonto enable it. - 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.
Create a file at ~/.gemini/statusline.py and paste the following Python script:
import json, sys, datetime, os, subprocess, time, urllib.parse
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 = '\033[0m'
G = '\033[32m' # green - user
C = '\033[36m' # cyan - model, branch
Y = '\033[33m' # yellow - ctx
DM = '\033[2m' # dim - git status
M = '\033[35m' # magenta - path
TM = '\033[97m' # bright white - time
LD = '\033[91m' # bright red - day limit pct
DLD = '\033[38;5;174m' # muted red - day reset time
try:
data = json.load(sys.stdin)
except:
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') as f:
model = json.load(f).get('model')
except Exception:
pass
if not model:
model = os.environ.get('AGY_MODEL', 'Gemini')
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)
total_str = f' of {fmt_num(ctx_size)}' if ctx_size else ''
ctx_str += f' ({fmt_num(left)}{total_str})'
else:
ctx_str = 'ctx:?'
# Quota / Rate limits
fh_used = None
fh_reset_str = None
cache_path = os.path.join(os.path.expanduser('~'), '.gemini', 'tmp', 'quota_cache.json')
try:
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
log_path = None
try:
import psutil
pid = os.getpid()
while pid > 1:
p = psutil.Process(pid)
p_name = p.name().lower()
if 'agy' in p_name or 'antigravity' in p_name:
for f in p.open_files():
if 'cli-' in f.path and f.path.endswith('.log'):
log_path = f.path
break
if log_path:
break
pid = p.ppid()
except Exception:
pass
def run_fetch(sync=False):
fetch_script = os.path.join(os.path.expanduser('~'), '.gemini', 'fetch_quota.py')
args = [sys.executable, fetch_script, '--model', model]
if log_path:
args.extend(['--log', log_path])
import subprocess
if sync:
try:
subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=1.0)
except:
pass
else:
if os.name == 'nt':
subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP)
else:
subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
if os.path.exists(cache_path):
mtime = os.path.getmtime(cache_path)
age = time.time() - mtime
if age > 1 * 60:
os.utime(cache_path, None)
if age > 5 * 60:
run_fetch(sync=True)
else:
run_fetch(sync=False)
with open(cache_path, 'r') as f:
quota_data = json.load(f)
mqs = quota_data.get('model_quotas', {})
mq = mqs.get(model)
if not mq:
for k, v in mqs.items():
if model_lower in k.lower() or k.lower() in model_lower:
mq = v
break
if mq:
rem_frac = mq.get('remainingFraction', 1.0)
fh_used = (1.0 - rem_frac) * 100.0
fh_reset_str = mq.get('resetTime')
# Safety net: If cache returned a stale reset time, see if stdin native data has fresh data!
if fh_reset_str:
try:
r_fixed = fh_reset_str.replace('Z', '+00:00')
r_dt = datetime.datetime.fromisoformat(r_fixed).astimezone()
now = datetime.datetime.now(datetime.timezone.utc)
if (r_dt - now).total_seconds() <= 0:
quota_obj = data.get('quota')
if quota_obj:
is_gemini = 'gemini' in model_lower
key_5h = 'gemini-5h' if is_gemini else '3p-5h'
q = quota_obj.get(key_5h)
if not q:
for k, v in quota_obj.items():
if '5h' in k:
q = v
break
if q:
n_reset = q.get('reset_time')
if n_reset:
n_fixed = n_reset.replace('Z', '+00:00')
n_dt = datetime.datetime.fromisoformat(n_fixed).astimezone()
if (n_dt - now).total_seconds() > 0:
rem_frac = q.get('remaining_fraction', 1.0)
fh_used = (1.0 - rem_frac) * 100.0
fh_reset_str = n_reset
except:
pass
else:
temp_path = cache_path + f'.{os.getpid()}.tmp'
with open(temp_path, 'w') as f:
json.dump({'timestamp': time.time()}, f)
try:
os.replace(temp_path, cache_path)
except:
pass
run_fetch(sync=True)
except Exception:
pass
def fmt_day_limit(reset_str, used_pct):
if used_pct is None:
return f'{LD}d-?{R}'
pct_text = f'd-{round(used_pct)}%'
if not reset_str:
return f'{LD}{pct_text}{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_text}{R} {DLD}{reset_dt.strftime("%H:%M")}{R} {LD}{dur}{R}'
else:
return f'{LD}d-?{R}'
except:
pass
return f'{LD}{pct_text}{R}'
day_str = f" {fmt_day_limit(fh_reset_str, fh_used)}"
# 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().strip() or '?'
status_out = subprocess.check_output(
['git', '-C', cwd, 'status', '--porcelain'],
stderr=subprocess.DEVNULL, timeout=1
).decode().strip()
if status_out:
lines = status_out.splitlines()
staged = sum(1 for l in lines if l[0] in 'MADR')
modified = sum(1 for l in lines if l[1] in 'MD')
untracked = sum(1 for l in lines if l.startswith('??'))
parts = []
if staged: parts.append(f'+{staged}')
if modified: parts.append(f'~{modified}')
if untracked: parts.append(f'?{untracked}')
git_status = ' '.join(parts)
else:
git_status = 'clean'
except:
branch = ''
git_status = ''
now_str = datetime.datetime.now().strftime('%H:%M')
account = data.get('email', '')
home = os.path.expanduser('~')
if os.name == 'nt':
home = home.replace('\\', '/')
cwd = cwd.replace('\\', '/')
if cwd.lower().startswith(home.lower()):
display_cwd = '~' + cwd[len(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:
item = json.loads(line)
display = item.get('display', '')
if display.startswith('/rename '):
conv_name = display[8:].strip()
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' {DM}{git_status}{R}' if git_status and git_status != 'clean' else f' {G}clean{R}') if branch else ''
account_str = f' {G}{account}{R}' if account else ''
print(f'{TM}{now_str}{R}{name_str} {C}{model}{R} {Y}{ctx_str}{R}{day_str} {path_part}{branch_str}{gs_str}{account_str}')Create a file at ~/.gemini/fetch_quota.py and paste the following code:
import os
import json
import time
import glob
import re
import urllib.request
import ssl
from datetime import datetime, timezone
import sys
def get_latest_log_file():
log_dir = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity-cli', 'log')
log_files = glob.glob(os.path.join(log_dir, 'cli-*.log'))
if not log_files:
return None
latest = max(log_files, key=os.path.getmtime)
return latest
def get_port_from_log(log_file):
if not log_file:
return None
try:
with open(log_file, 'r', encoding='utf-8') as f:
for line in f:
match = re.search(r'Language server listening on random port at (\d+) for HTTPS', line)
if match:
port = match.group(1)
return port
except Exception as e:
pass
return None
def fetch_status(port):
url = f"https://127.0.0.1:{port}/exa.language_server_pb.LanguageServerService/GetUserStatus"
data = b"{}"
req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}, method='POST')
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
with urllib.request.urlopen(req, context=ctx, timeout=2.0) as response:
return json.loads(response.read().decode('utf-8'))
except Exception as e:
return None
def main():
log_file = None
target_model = None
args = sys.argv[1:]
if '--model' in args:
idx = args.index('--model')
if idx + 1 < len(args):
target_model = args[idx+1].lower()
if '--log' in args:
idx = args.index('--log')
if idx + 1 < len(args):
log_file = args[idx+1]
if not log_file:
log_file = get_latest_log_file()
port = get_port_from_log(log_file)
if not port:
return
model_quotas = {}
# Polling loop to wait for fresh quota if it's currently stale (up to ~0.6s)
for attempt in range(4):
status_data = fetch_status(port)
if not status_data:
break
user_status = status_data.get('userStatus', {})
client_configs = user_status.get('cascadeModelConfigData', {}).get('clientModelConfigs', [])
current_quotas = {}
target_mq = None
for config in client_configs:
label = config.get('label')
quota_info = config.get('quotaInfo', {})
if label and quota_info:
mq = {
'remainingFraction': quota_info.get('remainingFraction', 1.0),
'resetTime': quota_info.get('resetTime')
}
current_quotas[label] = mq
if target_model and (target_model in label.lower() or label.lower() in target_model):
target_mq = mq
model_quotas = current_quotas
# If we have a target model, check if its reset time is in the future
if target_mq:
reset_str = target_mq.get('resetTime')
if reset_str:
try:
reset_str_fixed = reset_str.replace('Z', '+00:00')
reset_dt = datetime.fromisoformat(reset_str_fixed).astimezone()
now = datetime.now(timezone.utc)
if (reset_dt - now).total_seconds() > 0:
break # It's fresh! Break out of polling loop.
except:
pass
time.sleep(0.15) # Wait for Language Server to finish parsing response headers
if not model_quotas:
return
cache_path = os.path.join(os.path.expanduser('~'), '.gemini', 'tmp', 'quota_cache.json')
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
temp_path = cache_path + f'.{os.getpid()}.tmp'
with open(temp_path, 'w') as f:
json.dump({
'model_quotas': model_quotas,
'timestamp': time.time()
}, f)
try:
os.replace(temp_path, cache_path)
except:
pass
if __name__ == '__main__':
main()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.