Skip to content

Instantly share code, notes, and snippets.

@it3xl
Last active June 21, 2026 21:11
Show Gist options
  • Select an option

  • Save it3xl/3d7b91d6b8fa3c790fdbcfc0e8d0321b to your computer and use it in GitHub Desktop.

Select an option

Save it3xl/3d7b91d6b8fa3c790fdbcfc0e8d0321b to your computer and use it in GitHub Desktop.
Antigravity CLI Custom Status Line Guide

Antigravity CLI Custom Status Line Guide

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 as clean.
  • my@email.com: Active authenticated Google account.

Approach Description

This refined approach handles the latest CLI features robustly:

  1. statusline.py: The core formatter. It reads the JSON output provided natively by the CLI on stdin and 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).
  2. 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!
  3. Configuration Automation: Setup is a single settings edit.

🤖 Instructions for Antigravity (AI Agent)

If you are the AI agent reading this Gist to fulfill an installation request, you MUST execute the following deployment protocol:

  1. Analyze & Adapt: Review the provided statusline.py and fetch_quota.py scripts 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.
  2. 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 agy status API, and execute it to verify the output doesn't crash or error.
  3. 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.py and ~/.gemini/fetch_quota.py, then configure ~/.gemini/antigravity-cli/settings.json to enable it.
  4. Confirmation: After completing the setup, tell the user that the custom status line is successfully installed. Based on your knowledge of the current agy CLI version, advise the user on how to make it appear (for example, they may need to hit Enter to refresh the prompt, or run the /statusline command to toggle/enable it). Finally, ask them to report back if it still doesn't show up so you can troubleshoot.

1. Setup the Formatter (statusline.py)

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}')

2. Setup the Quota Fetcher (fetch_quota.py)

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()

3. Configure Antigravity to Use the Status Line

You don't need any shell wrapper scripts! You can configure Antigravity to call the Python script directly.

On Linux / macOS

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.json

On Windows

Open 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment