From 7cac3bc0e4c6514f49900c9d2751df9a1ff5d390 Mon Sep 17 00:00:00 2001 From: weby-homelab Date: Thu, 28 May 2026 10:07:50 +0300 Subject: [PATCH 1/3] feat(examples): add setup.sh scripts for statusline and title --- README.md | 61 ++++++++++++++++++++++ examples/statusline/README.md | 88 +++++++++++++++++++++++++++---- examples/statusline/setup.sh | 98 +++++++++++++++++++++++++++++++++++ examples/title/README.md | 41 +++++++++++++++ examples/title/setup.sh | 98 +++++++++++++++++++++++++++++++++++ 5 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 examples/statusline/setup.sh create mode 100644 examples/title/setup.sh diff --git a/README.md b/README.md index aeff0f6bf..1aa9d318b 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,67 @@ The CLI authenticates via the system keyring, falling back to Google Sign-In if --- +## Customization + +### Custom Status Line + +By default, the CLI displays a minimal status bar showing only `? for shortcuts` and the current model name. You can replace it with a fully dynamic status line that shows agent state, context window usage, Git branch, active subagents, and more. + +**Quick setup** — automatically copy and configure the statusline script: + +```bash +bash examples/statusline/setup.sh +``` + +Or configure it manually in your settings (accessible via `/settings` in the CLI, or by editing the settings file directly): + +```json +{ + "statusLine": { + "command": "/absolute/path/to/statusline.sh", + "enabled": true + } +} +``` + +> [!TIP] +> The settings file location varies by platform: +> - **Linux**: `~/.gemini/antigravity-cli/settings.json` +> - **macOS**: `~/Library/Application Support/antigravity-cli/settings.json` +> - **Windows**: `%APPDATA%\antigravity-cli\settings.json` +> +> You can also open settings interactively by typing `/settings` inside the CLI. + +The script receives a JSON payload on stdin with the current agent state and outputs formatted ANSI text. See [`examples/statusline/`](examples/statusline/) for the full reference implementation. + +### Custom Window Title + +Similarly, you can set a dynamic terminal window title that reflects the agent's current state (thinking, tool use, idle) and workspace. + +**Quick setup** — automatically copy and configure the title script: + +```bash +bash examples/title/setup.sh +``` + +Or configure it manually in your settings: + +```json +{ + "title": { + "command": "/absolute/path/to/title.sh", + "enabled": true + } +} +``` + +See [`examples/title/`](examples/title/) for details. + +> [!NOTE] +> Both scripts require [`jq`](https://jqlang.org/) to be installed. Most Linux distributions include it by default; on macOS, install via `brew install jq`. + +--- + ## Terms of Service & Data Use > [!WARNING] diff --git a/examples/statusline/README.md b/examples/statusline/README.md index 923d66d2d..c53bbb0d9 100644 --- a/examples/statusline/README.md +++ b/examples/statusline/README.md @@ -1,21 +1,79 @@ -# CLI Statusline Example +# Custom Status Line -This directory contains an example script (`statusline.sh`) that demonstrates how to create a custom, dynamic statusline for the Antigravity CLI. +This directory contains a reference implementation (`statusline.sh`) for a custom, dynamic status line for the Antigravity CLI. -For more details on how to use and configure the statusline script, please refer to the official public documentation: -[https://antigravity.google/docs/cli-statusline](https://antigravity.google/docs/cli-statusline) +## Quick Start + +### Option 1: Automatic Setup (Recommended) + +Run the included setup script from the root of the repository: + +```bash +bash examples/statusline/setup.sh +``` + +This script will automatically: +1. Copy `statusline.sh` to your platform's global settings directory (so it stays configured even if you move or delete this repository). +2. Configure and enable it in your global `settings.json` file. + +### Option 2: Manual configuration + +1. Copy `statusline.sh` to a directory of your choice. +2. Edit your `settings.json` file to point `statusLine.command` to the absolute path of `statusline.sh` and set `statusLine.enabled` to `true`: + +```json +{ + "statusLine": { + "command": "/absolute/path/to/statusline.sh", + "enabled": true + } +} +``` + +**Settings file locations:** + +| Platform | Path | +| :--- | :--- | +| Linux | `~/.gemini/antigravity-cli/settings.json` | +| macOS | `~/Library/Application Support/antigravity-cli/settings.json` | +| Windows | `%APPDATA%\antigravity-cli\settings.json` | + +> [!IMPORTANT] +> The `command` field must be an **absolute path** to the script. Relative paths and `~` expansion are not supported. + +After saving, restart `agy` for changes to take effect. ## How it works -The `statusline.sh` script reads a JSON payload from standard input, containing various state information from the CLI. It then: -1. Extracts multiple fields like `agent_state`, `vcs` info, context usage, and terminal dimensions using `jq`. -2. Computes visual indicators, such as a Unicode progress bar for context window usage. -3. Formats the data with standard ANSI 16-color codes for visual distinction. -4. Dynamically adjusts the layout to be 1 or 2 lines based on the available terminal width. +The CLI pipes a JSON payload into the script's stdin on each state change. The script: + +1. Extracts fields like `agent_state`, `vcs` info, `context_window` usage, and `terminal_width` using `jq`. +2. Computes visual indicators (e.g., a Unicode progress bar for context window usage). +3. Formats the output using standard ANSI 16-color codes. +4. Dynamically adjusts the layout based on the available terminal width (single-line for wide terminals, two-line for narrower ones). + +### JSON payload fields + +| Field | Type | Description | +| :--- | :--- | :--- | +| `agent_state` | string | Current state: `idle`, `thinking`, `working`, `tool_use` | +| `context_window.used_percentage` | number | Context window utilization (0–100) | +| `vcs.branch` | string | Current Git branch name | +| `vcs.dirty` | boolean | Whether the working tree has uncommitted changes | +| `sandbox.enabled` | boolean | Whether sandbox mode is active | +| `artifact_count` | number | Number of artifacts in the current session | +| `subagents` | array | List of active subagents | +| `task_count` | number | Number of background tasks | +| `model.display_name` | string | Human-readable model name | +| `terminal_width` | number | Current terminal width in columns | + +### Prerequisites + +- [`jq`](https://jqlang.org/) must be installed and available in `$PATH`. ## Examples -### Default Statusline +### Default Status Line ![Default Statusline](images/statusline-default.png) ### Review Mode @@ -23,3 +81,13 @@ The `statusline.sh` script reads a JSON payload from standard input, containing ### Tool Execution ![Tool Execution Statusline](images/statusline-tool.png) + +## Writing your own + +You can use `statusline.sh` as a starting point. The only contract is: + +1. Read JSON from stdin. +2. Write one or more lines of ANSI-formatted text to stdout. +3. Exit with code 0. + +For the official documentation, see [antigravity.google/docs/cli-statusline](https://antigravity.google/docs/cli-statusline). diff --git a/examples/statusline/setup.sh b/examples/statusline/setup.sh new file mode 100644 index 000000000..584c78044 --- /dev/null +++ b/examples/statusline/setup.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# setup.sh - Installs and enables the custom statusline for Antigravity CLI + +set -euo pipefail + +# 1. Determine the settings directory +OS="$(uname -s)" +case "${OS}" in + Darwin*) + CONFIG_DIR="$HOME/Library/Application Support/antigravity-cli" + ;; + Linux*) + CONFIG_DIR="$HOME/.gemini/antigravity-cli" + ;; + CYGWIN*|MINGW*|MSYS*) + # Windows environments (Git Bash, MSYS) + if [ -n "${APPDATA:-}" ]; then + CONFIG_DIR="${APPDATA}/antigravity-cli" + else + CONFIG_DIR="$HOME/AppData/Roaming/antigravity-cli" + fi + ;; + *) + # Default fallback to Linux path + CONFIG_DIR="$HOME/.gemini/antigravity-cli" + ;; +esac + +# Convert config dir to absolute path if needed +mkdir -p "$CONFIG_DIR" +CONFIG_DIR="$(cd "$CONFIG_DIR" && pwd)" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SOURCE_SCRIPT="$SCRIPT_DIR/statusline.sh" +TARGET_SCRIPT="$CONFIG_DIR/statusline.sh" + +echo "Installing statusline script to: $TARGET_SCRIPT" +cp "$SOURCE_SCRIPT" "$TARGET_SCRIPT" +chmod +x "$TARGET_SCRIPT" + +SETTINGS_FILE="$CONFIG_DIR/settings.json" +echo "Configuring settings file at: $SETTINGS_FILE" + +# Make sure settings.json exists +if [ ! -f "$SETTINGS_FILE" ]; then + echo "{}" > "$SETTINGS_FILE" +fi + +# Update settings.json using python if available, otherwise fallback to jq +if command -v python3 >/dev/null 2>&1; then + python3 -c ' +import json, sys +file_path, script_path = sys.argv[1], sys.argv[2] +try: + with open(file_path, "r") as f: + data = json.load(f) +except Exception: + data = {} +if "statusLine" not in data or not isinstance(data["statusLine"], dict): + data["statusLine"] = {} +data["statusLine"]["command"] = script_path +data["statusLine"]["enabled"] = True +with open(file_path, "w") as f: + json.dump(data, f, indent=2) +' "$SETTINGS_FILE" "$TARGET_SCRIPT" +elif command -v python >/dev/null 2>&1; then + python -c ' +import json, sys +file_path, script_path = sys.argv[1], sys.argv[2] +try: + with open(file_path, "r") as f: + data = json.load(f) +except Exception: + data = {} +if "statusLine" not in data or not isinstance(data["statusLine"], dict): + data["statusLine"] = {} +data["statusLine"]["command"] = script_path +data["statusLine"]["enabled"] = True +with open(file_path, "w") as f: + json.dump(data, f, indent=2) +' "$SETTINGS_FILE" "$TARGET_SCRIPT" +elif command -v jq >/dev/null 2>&1; then + TEMP_FILE=$(mktemp) + jq --arg cmd "$TARGET_SCRIPT" '.statusLine = ((.statusLine // {}) + {command: $cmd, enabled: true})' "$SETTINGS_FILE" > "$TEMP_FILE" + mv "$TEMP_FILE" "$SETTINGS_FILE" +else + echo "Error: Neither python3, python, nor jq is installed. Please manually add the following to your $SETTINGS_FILE:" + echo "{" + echo " \"statusLine\": {" + echo " \"command\": \"$TARGET_SCRIPT\"," + echo " \"enabled\": true" + echo " }" + echo "}" + exit 1 +fi + +echo "Status line successfully installed and enabled!" +echo "Please restart Antigravity CLI (agy) to see the changes." diff --git a/examples/title/README.md b/examples/title/README.md index 1430ae61b..f6da88e67 100644 --- a/examples/title/README.md +++ b/examples/title/README.md @@ -5,6 +5,47 @@ This directory contains an example script (`title.sh`) that demonstrates how to For more details on how to use and configure the title script, please refer to the official public documentation: [https://antigravity.google/docs/cli-title](https://antigravity.google/docs/cli-title) +## Quick Start + +### Option 1: Automatic Setup (Recommended) + +Run the included setup script from the root of the repository: + +```bash +bash examples/title/setup.sh +``` + +This script will automatically: +1. Copy `title.sh` to your platform's global settings directory (so it stays configured even if you move or delete this repository). +2. Configure and enable it in your global `settings.json` file. + +### Option 2: Manual configuration + +1. Copy `title.sh` to a directory of your choice. +2. Edit your `settings.json` file to point `title.command` to the absolute path of `title.sh` and set `title.enabled` to `true`: + +```json +{ + "title": { + "command": "/absolute/path/to/title.sh", + "enabled": true + } +} +``` + +**Settings file locations:** + +| Platform | Path | +| :--- | :--- | +| Linux | `~/.gemini/antigravity-cli/settings.json` | +| macOS | `~/Library/Application Support/antigravity-cli/settings.json` | +| Windows | `%APPDATA%\antigravity-cli\settings.json` | + +> [!IMPORTANT] +> The `command` field must be an **absolute path** to the script. Relative paths and `~` expansion are not supported. + +After saving, restart `agy` for changes to take effect. + ## How it works The `title.sh` script reads a JSON payload from standard input, which contains real-time information about the agent's state and context. It then: diff --git a/examples/title/setup.sh b/examples/title/setup.sh new file mode 100644 index 000000000..1e7bd96cd --- /dev/null +++ b/examples/title/setup.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# setup.sh - Installs and enables the custom window title for Antigravity CLI + +set -euo pipefail + +# 1. Determine the settings directory +OS="$(uname -s)" +case "${OS}" in + Darwin*) + CONFIG_DIR="$HOME/Library/Application Support/antigravity-cli" + ;; + Linux*) + CONFIG_DIR="$HOME/.gemini/antigravity-cli" + ;; + CYGWIN*|MINGW*|MSYS*) + # Windows environments (Git Bash, MSYS) + if [ -n "${APPDATA:-}" ]; then + CONFIG_DIR="${APPDATA}/antigravity-cli" + else + CONFIG_DIR="$HOME/AppData/Roaming/antigravity-cli" + fi + ;; + *) + # Default fallback to Linux path + CONFIG_DIR="$HOME/.gemini/antigravity-cli" + ;; +esac + +# Convert config dir to absolute path if needed +mkdir -p "$CONFIG_DIR" +CONFIG_DIR="$(cd "$CONFIG_DIR" && pwd)" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SOURCE_SCRIPT="$SCRIPT_DIR/title.sh" +TARGET_SCRIPT="$CONFIG_DIR/title.sh" + +echo "Installing title script to: $TARGET_SCRIPT" +cp "$SOURCE_SCRIPT" "$TARGET_SCRIPT" +chmod +x "$TARGET_SCRIPT" + +SETTINGS_FILE="$CONFIG_DIR/settings.json" +echo "Configuring settings file at: $SETTINGS_FILE" + +# Make sure settings.json exists +if [ ! -f "$SETTINGS_FILE" ]; then + echo "{}" > "$SETTINGS_FILE" +fi + +# Update settings.json using python if available, otherwise fallback to jq +if command -v python3 >/dev/null 2>&1; then + python3 -c ' +import json, sys +file_path, script_path = sys.argv[1], sys.argv[2] +try: + with open(file_path, "r") as f: + data = json.load(f) +except Exception: + data = {} +if "title" not in data or not isinstance(data["title"], dict): + data["title"] = {} +data["title"]["command"] = script_path +data["title"]["enabled"] = True +with open(file_path, "w") as f: + json.dump(data, f, indent=2) +' "$SETTINGS_FILE" "$TARGET_SCRIPT" +elif command -v python >/dev/null 2>&1; then + python -c ' +import json, sys +file_path, script_path = sys.argv[1], sys.argv[2] +try: + with open(file_path, "r") as f: + data = json.load(f) +except Exception: + data = {} +if "title" not in data or not isinstance(data["title"], dict): + data["title"] = {} +data["title"]["command"] = script_path +data["title"]["enabled"] = True +with open(file_path, "w") as f: + json.dump(data, f, indent=2) +' "$SETTINGS_FILE" "$TARGET_SCRIPT" +elif command -v jq >/dev/null 2>&1; then + TEMP_FILE=$(mktemp) + jq --arg cmd "$TARGET_SCRIPT" '.title = ((.title // {}) + {command: $cmd, enabled: true})' "$SETTINGS_FILE" > "$TEMP_FILE" + mv "$TEMP_FILE" "$SETTINGS_FILE" +else + echo "Error: Neither python3, python, nor jq is installed. Please manually add the following to your $SETTINGS_FILE:" + echo "{" + echo " \"title\": {" + echo " \"command\": \"$TARGET_SCRIPT\"," + echo " \"enabled\": true" + echo " }" + echo "}" + exit 1 +fi + +echo "Terminal window title script successfully installed and enabled!" +echo "Please restart Antigravity CLI (agy) to see the changes." From 3263dfb0de980bd8bb30a245c047e40672616c90 Mon Sep 17 00:00:00 2001 From: weby-homelab Date: Thu, 28 May 2026 22:12:25 +0300 Subject: [PATCH 2/3] feat(examples): add PowerShell, Fish, and Node.js cross-platform statusline and title scripts --- examples/statusline/README.md | 52 ++++++- examples/statusline/statusline.fish | 225 ++++++++++++++++++++++++++++ examples/statusline/statusline.js | 220 +++++++++++++++++++++++++++ examples/statusline/statusline.ps1 | 223 +++++++++++++++++++++++++++ examples/title/README.md | 59 ++++++-- examples/title/title.fish | 37 +++++ examples/title/title.js | 53 +++++++ examples/title/title.ps1 | 39 +++++ 8 files changed, 891 insertions(+), 17 deletions(-) create mode 100755 examples/statusline/statusline.fish create mode 100755 examples/statusline/statusline.js create mode 100644 examples/statusline/statusline.ps1 create mode 100755 examples/title/title.fish create mode 100755 examples/title/title.js create mode 100644 examples/title/title.ps1 diff --git a/examples/statusline/README.md b/examples/statusline/README.md index c53bbb0d9..6b3a6ed88 100644 --- a/examples/statusline/README.md +++ b/examples/statusline/README.md @@ -1,10 +1,15 @@ # Custom Status Line -This directory contains a reference implementation (`statusline.sh`) for a custom, dynamic status line for the Antigravity CLI. +This directory contains reference implementations for a custom, dynamic status line for the Antigravity CLI across different shell environments: + +1. **`statusline.sh`** (Bash/Zsh - Linux/macOS) +2. **`statusline.js`** (Node.js - Cross-platform: Windows/Linux/macOS) +3. **`statusline.ps1`** (PowerShell - Windows PowerShell or PowerShell Core `pwsh`) +4. **`statusline.fish`** (Fish shell - Linux/macOS) ## Quick Start -### Option 1: Automatic Setup (Recommended) +### Option 1: Automatic Setup (Recommended for Bash/Zsh) Run the included setup script from the root of the repository: @@ -18,9 +23,10 @@ This script will automatically: ### Option 2: Manual configuration -1. Copy `statusline.sh` to a directory of your choice. -2. Edit your `settings.json` file to point `statusLine.command` to the absolute path of `statusline.sh` and set `statusLine.enabled` to `true`: +1. Copy the script corresponding to your shell to a directory of your choice. +2. Edit your `settings.json` file to point `statusLine.command` to the absolute path of the script and set `statusLine.enabled` to `true`: +#### Bash/Zsh (Linux/macOS) ```json { "statusLine": { @@ -30,6 +36,37 @@ This script will automatically: } ``` +#### Node.js (Cross-platform) +```json +{ + "statusLine": { + "command": "node /absolute/path/to/statusline.js", + "enabled": true + } +} +``` + +#### PowerShell (Windows / pwsh) +```json +{ + "statusLine": { + "command": "powershell.exe -ExecutionPolicy Bypass -File C:\\absolute\\path\\to\\statusline.ps1", + "enabled": true + } +} +``` +*For PowerShell Core, use `pwsh.exe` or `pwsh` instead of `powershell.exe`.* + +#### Fish Shell +```json +{ + "statusLine": { + "command": "/absolute/path/to/statusline.fish", + "enabled": true + } +} +``` + **Settings file locations:** | Platform | Path | @@ -47,7 +84,7 @@ After saving, restart `agy` for changes to take effect. The CLI pipes a JSON payload into the script's stdin on each state change. The script: -1. Extracts fields like `agent_state`, `vcs` info, `context_window` usage, and `terminal_width` using `jq`. +1. Extracts fields like `agent_state`, `vcs` info, `context_window` usage, and `terminal_width`. 2. Computes visual indicators (e.g., a Unicode progress bar for context window usage). 3. Formats the output using standard ANSI 16-color codes. 4. Dynamically adjusts the layout based on the available terminal width (single-line for wide terminals, two-line for narrower ones). @@ -69,7 +106,8 @@ The CLI pipes a JSON payload into the script's stdin on each state change. The s ### Prerequisites -- [`jq`](https://jqlang.org/) must be installed and available in `$PATH`. +- **Bash (`statusline.sh`) & Fish (`statusline.fish`)**: Require [`jq`](https://jqlang.org/) to be installed and available in `$PATH`. +- **Node.js (`statusline.js`) & PowerShell (`statusline.ps1`)**: Have **zero external dependencies** and work out-of-the-box. ## Examples @@ -84,7 +122,7 @@ The CLI pipes a JSON payload into the script's stdin on each state change. The s ## Writing your own -You can use `statusline.sh` as a starting point. The only contract is: +You can use any of the provided scripts as a starting point. The only contract is: 1. Read JSON from stdin. 2. Write one or more lines of ANSI-formatted text to stdout. diff --git a/examples/statusline/statusline.fish b/examples/statusline/statusline.fish new file mode 100755 index 000000000..cfebf65c6 --- /dev/null +++ b/examples/statusline/statusline.fish @@ -0,0 +1,225 @@ +#!/usr/bin/fish + +# Read JSON payload from stdin +set -l DATA (cat) + +# Extract fields using jq. Since Fish is on Unix, jq will be available. +if not type -q jq + echo "READY" + echo "ctx · 0%" + exit 0 +end + +set -l STATE (echo $DATA | jq -r '.agent_state // "idle"') +set -l USED_PCT (echo $DATA | jq -r '.context_window.used_percentage // 0') +set -l VCS_BRANCH (echo $DATA | jq -r '.vcs.branch // ""') +set -l VCS_DIRTY (echo $DATA | jq -r '.vcs.dirty // false') +set -l VCS_TYPE (echo $DATA | jq -r '.vcs.type // ""') +set -l SANDBOX (echo $DATA | jq -r '.sandbox.enabled // false') +set -l SANDBOX_NET (echo $DATA | jq -r '.sandbox.allow_network // false') +set -l ARTIFACTS (echo $DATA | jq -r '.artifact_count // 0') +set -l SUBAGENTS (echo $DATA | jq -r 'if .subagents | type == "array" then (.subagents | length) else 0 end') +set -l BG_TASKS (echo $DATA | jq -r '.task_count // 0') +set -l MODEL_ID (echo $DATA | jq -r '.model.id // ""') +set -l MODEL_NAME (echo $DATA | jq -r '.model.display_name // ""') +set -l COLS (echo $DATA | jq -r '.terminal_width // 80') +set -l CWD (echo $DATA | jq -r '.cwd // ""') +set -l CONV_ID (echo $DATA | jq -r '.conversation_id // ""') +set -l INPUT_TOKENS (echo $DATA | jq -r '.context_window.total_input_tokens // 0') +set -l OUTPUT_TOKENS (echo $DATA | jq -r '.context_window.total_output_tokens // 0') +set -l CTX_LIMIT (echo $DATA | jq -r '.context_window.context_window_size // 0') +set -l CTX_USED (echo $DATA | jq -r '.context_window.current_usage // 0') + +# ANSI Helpers +set -l R (set_color normal) +set -l B (set_color -o) +set -l I (set_color -i) + +set -l FG_RED (set_color red) +set -l FG_GREEN (set_color green) +set -l FG_YELLOW (set_color yellow) +set -l FG_BLUE (set_color blue) +set -l FG_MAGENTA (set_color magenta) +set -l FG_CYAN (set_color cyan) +set -l FG_WHITE (set_color white) +set -l FG_GRAY (set_color 909090) + +set -l FG_BRIGHT_RED (set_color brred) +set -l FG_BRIGHT_GREEN (set_color brgreen) +set -l FG_BRIGHT_YELLOW (set_color bryellow) +set -l FG_BRIGHT_BLUE (set_color brblue) +set -l FG_BRIGHT_MAGENTA (set_color brmagenta) +set -l FG_BRIGHT_CYAN (set_color brcyan) +set -l FG_BRIGHT_WHITE (set_color brwhite) + +set -l NUM_COLOR "$FG_BRIGHT_WHITE$B" + +function human_format -a num + if test -z "$num"; or test "$num" -eq 0 + echo "0" + return + end + if test "$num" -ge 1000000 + set -l main (math -s0 "$num / 1000000") + set -l dec (math -s0 "($num % 1000000) / 100000") + echo "$main.$dec"M + elif test "$num" -ge 1000 + set -l main (math -s0 "$num / 1000") + set -l dec (math -s0 "($num % 1000) / 100") + echo "$main.$dec"K + else + echo "$num" + end +end + +set -l INPUT_TOK_FMT (human_format $INPUT_TOKENS) +set -l OUTPUT_TOK_FMT (human_format $OUTPUT_TOKENS) +set -l CTX_LIMIT_FMT (human_format $CTX_LIMIT) +set -l CTX_USED_FMT (human_format $CTX_USED) + +function shorten_path -a path + if test -z "$path" + echo "" + return + end + set -l shortened (string replace -r "^$HOME" "~" $path) + if test (string length $shortened) -gt 25 + set -l base_name (basename $shortened) + echo "...$base_name" + else + echo $shortened + end +end +set -l CWD_SHORT (shorten_path $CWD) + +# State Indicator +set -l S "" +switch $STATE + case idle + set S "$FG_BRIGHT_GREEN$B● READY$R" + case thinking + set S "$FG_BRIGHT_YELLOW$B◆ THINKING$R" + case working + set S "$FG_BRIGHT_CYAN$B⚙ WORKING$R" + case tool_use + set S "$FG_BRIGHT_MAGENTA$B🔧 TOOL$R" + case '*' + set -l upper_state (string upper $STATE) + set S "$FG_WHITE$B⏳ $upper_state$R" +end + +# VCS Branch & Type +set -l V "" +if test -n "$VCS_BRANCH" + set -l vcs_label "git" + if test -n "$VCS_TYPE" + set vcs_label $VCS_TYPE + end + if test "$VCS_DIRTY" = "true" + set V "$FG_GRAY ╱ $FG_GRAY$vcs_label:$FG_BRIGHT_RED$VCS_BRANCH$FG_BRIGHT_YELLOW*$R" + else + set V "$FG_GRAY ╱ $FG_GRAY$vcs_label:$FG_BRIGHT_BLUE$VCS_BRANCH$R" + end +end + +# Model +set -l MODEL_DISP $MODEL_NAME +if test -z "$MODEL_DISP" + set MODEL_DISP $MODEL_ID +end +set -l M "" +if test -n "$MODEL_DISP" + set M "$FG_GRAY ╱ $FG_BRIGHT_MAGENTA$I$MODEL_DISP$R" +end + +# Sandbox Badge +set -l SB "" +if test "$SANDBOX" = "true" + if test "$SANDBOX_NET" = "true" + set SB "$FG_GRAY🛡️ sandbox $FG_BRIGHT_GREEN$B"ON (net)"$R" + else + set SB "$FG_GRAY🛡️ sandbox $FG_BRIGHT_GREEN$B"ON (no-net)"$R" + end +else + set SB "$FG_GRAY🛡️ sandbox off$R" +end + +# Context Bar +set -l BAR_LEN 15 +set -l PCT_INT (math -s0 "$USED_PCT") +set -l FILLED (math -s0 "$PCT_INT * $BAR_LEN / 100") +set -l REMAINDER (math -s0 "($PCT_INT * $BAR_LEN) % 100") + +set -l BAR_COLOR $FG_BRIGHT_WHITE +if test "$PCT_INT" -ge 90 + set BAR_COLOR $FG_BRIGHT_RED +elif test "$PCT_INT" -ge 60 + set BAR_COLOR $FG_BRIGHT_YELLOW +end + +set -l BAR "" +for i in (seq 0 (math "$BAR_LEN - 1")) + if test "$i" -lt "$FILLED" + set BAR "$BAR"█ + elif test "$i" -eq "$FILLED" + if test "$REMAINDER" -ge 75 + set BAR "$BAR"▓ + elif test "$REMAINDER" -ge 50 + set BAR "$BAR"▒ + elif test "$REMAINDER" -ge 25 + set BAR "$BAR"░ + else + set BAR "$BAR"· + end + else + set BAR "$BAR"· + end +end + +# Stats & Metadata +set -l PCT_FMT (printf "%.1f" $USED_PCT) +set -l CTX_BAR "$FG_GRAY"ctx "$BAR_COLOR$BAR $NUM_COLOR$PCT_FMT%$R" +set -l ART_FMT "$FG_GRAY"📦 "$NUM_COLOR$ARTIFACTS$R" +set -l SUB_FMT "$FG_GRAY"🤖 "$NUM_COLOR$SUBAGENTS$R" +set -l BG_FMT "$FG_GRAY"⏳ "$NUM_COLOR$BG_TASKS$R" + +set -l DIR_FMT "" +if test -n "$CWD_SHORT" + set DIR_FMT "$FG_GRAY ╱ 📂 $CWD_SHORT$R" +end + +set -l CONV_FMT "" +if test -n "$CONV_ID" + set -l sub_conv (string sub -l 8 $CONV_ID) + set CONV_FMT "$FG_GRAY ╱ id:$sub_conv$R" +end + +set -l TOK_DETAILS "" +if test "$CTX_USED" -gt 0 + set TOK_DETAILS " ($CTX_USED_FMT/$CTX_LIMIT_FMT)" +end + +set -l DOT "$FG_GRAY · $R" + +# Output Assembly +if test "$COLS" -ge 120 + set -l line1 "$S$M$V$DIR_FMT$CONV_FMT" + if test "$CTX_USED" -gt 0 + set TOK_DETAILS " ($CTX_USED_FMT/$CTX_LIMIT_FMT · $INPUT_TOK_FMT in/$OUTPUT_TOK_FMT out)" + end + set -l line2 " $CTX_BAR$TOK_DETAILS$DOT$ART_FMT$DOT$SUB_FMT$DOT$BG_FMT$DOT$SB" + echo -e "$line1$FG_GRAY │ $R$line2" +elif test "$COLS" -ge 80 + set -l line1 "$S$M$V$DIR_FMT" + set -l line2 " $CTX_BAR$TOK_DETAILS$DOT$ART_FMT$DOT$SUB_FMT$DOT$BG_FMT$DOT$SB" + echo -e "$FG_GRAY╭─$R $line1" + echo -e "$FG_GRAY╰─$R$line2" +else + set -l M_SHORT "" + if test -n "$MODEL_DISP" + set -l sub_model (string sub -l 12 $MODEL_DISP) + set M_SHORT "$FG_GRAY ╱ $FG_BRIGHT_MAGENTA$sub_model$R" + end + echo -e "$S$M_SHORT" + echo -e "$CTX_BAR$DOT$BG_FMT" +end diff --git a/examples/statusline/statusline.js b/examples/statusline/statusline.js new file mode 100755 index 000000000..74f812c3c --- /dev/null +++ b/examples/statusline/statusline.js @@ -0,0 +1,220 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// ─── Read JSON from stdin ──────────────────────────────────────────────────── +let rawData = ''; +try { + rawData = fs.readFileSync(0, 'utf-8'); +} catch (e) { + // Silence read errors +} + +let data = {}; +try { + data = JSON.parse(rawData || '{}'); +} catch (e) { + // Silence JSON parse errors +} + +// ─── ANSI Helpers (Standard 16-color palette only) ─────────────────────────── +const R = "\x1b[0m"; // Reset +const B = "\x1b[1m"; // Bold +const D = "\x1b[2m"; // Dim +const I = "\x1b[3m"; // Italic + +// Foreground accents (Standard 16 colors) +const FG_BLACK = "\x1b[30m"; +const FG_RED = "\x1b[31m"; +const FG_GREEN = "\x1b[32m"; +const FG_YELLOW = "\x1b[33m"; +const FG_BLUE = "\x1b[34m"; +const FG_MAGENTA = "\x1b[35m"; +const FG_CYAN = "\x1b[36m"; +const FG_WHITE = "\x1b[37m"; + +const FG_GRAY = "\x1b[90m"; +const FG_BRIGHT_RED = "\x1b[91m"; +const FG_BRIGHT_GREEN = "\x1b[92m"; +const FG_BRIGHT_YELLOW = "\x1b[93m"; +const FG_BRIGHT_BLUE = "\x1b[94m"; +const FG_BRIGHT_MAGENTA = "\x1b[95m"; +const FG_BRIGHT_CYAN = "\x1b[96m"; +const FG_BRIGHT_WHITE = "\x1b[97m"; + +const NUM_COLOR = FG_BRIGHT_WHITE + B; + +// ─── Extract fields with fallbacks ─────────────────────────────────────────── +const state = data.agent_state || "idle"; +const usedPct = (data.context_window && typeof data.context_window.used_percentage === 'number') ? data.context_window.used_percentage : 0; +const vcsBranch = (data.vcs && data.vcs.branch) ? data.vcs.branch : ""; +const vcsDirty = (data.vcs && data.vcs.dirty) ? data.vcs.dirty : false; +const vcsType = (data.vcs && data.vcs.type) ? data.vcs.type : ""; +const sandboxEnabled = (data.sandbox && data.sandbox.enabled) ? data.sandbox.enabled : false; +const sandboxNet = (data.sandbox && data.sandbox.allow_network) ? data.sandbox.allow_network : false; +const artifactCount = data.artifact_count || 0; +const subagentsCount = Array.isArray(data.subagents) ? data.subagents.length : 0; +const taskCount = data.task_count || 0; +const modelId = (data.model && data.model.id) ? data.model.id : ""; +const modelName = (data.model && data.model.display_name) ? data.model.display_name : ""; +const cols = data.terminal_width || 80; +const cwd = data.cwd || ""; +const convId = data.conversation_id || ""; +const inputTokens = (data.context_window && data.context_window.total_input_tokens) ? data.context_window.total_input_tokens : 0; +const outputTokens = (data.context_window && data.context_window.total_output_tokens) ? data.context_window.total_output_tokens : 0; +const ctxLimit = (data.context_window && data.context_window.context_window_size) ? data.context_window.context_window_size : 0; +const ctxUsed = (data.context_window && data.context_window.current_usage) ? data.context_window.current_usage : 0; + +// ─── Helper Formatting Functions ───────────────────────────────────────────── +function humanFormat(num) { + if (!num || isNaN(num) || num === 0) return "0"; + if (num >= 1000000) { + return (num / 1000000).toFixed(1).replace(/\.0$/, '') + "M"; + } + if (num >= 1000) { + return (num / 1000).toFixed(1).replace(/\.0$/, '') + "K"; + } + return num.toString(); +} + +function shortenPath(p) { + if (!p) return ""; + const home = os.homedir(); + if (p.startsWith(home)) { + p = "~" + p.slice(home.length); + } + if (p.length > 25) { + return "..." + path.basename(p); + } + return p; +} + +const cwdShort = shortenPath(cwd); + +// ─── State Indicator (No background colors) ────────────────────────────────── +let S = ""; +switch (state) { + case 'idle': + S = `${FG_BRIGHT_GREEN}${B}● READY${R}`; + break; + case 'thinking': + S = `${FG_BRIGHT_YELLOW}${B}◆ THINKING${R}`; + break; + case 'working': + S = `${FG_BRIGHT_CYAN}${B}⚙ WORKING${R}`; + break; + case 'tool_use': + S = `${FG_BRIGHT_MAGENTA}${B}🔧 TOOL${R}`; + break; + default: + S = `${FG_WHITE}${B}⏳ ${state.toUpperCase()}${R}`; +} + +// ─── VCS Branch & Type ─────────────────────────────────────────────────────── +let V = ""; +if (vcsBranch) { + const vcsLabel = vcsType || "git"; + if (vcsDirty || vcsDirty === "true") { + V = `${FG_GRAY} ╱ ${FG_GRAY}${vcsLabel}:${FG_BRIGHT_RED}${vcsBranch}${FG_BRIGHT_YELLOW}*${R}`; + } else { + V = `${FG_GRAY} ╱ ${FG_GRAY}${vcsLabel}:${FG_BRIGHT_BLUE}${vcsBranch}${R}`; + } +} + +// ─── Model ─────────────────────────────────────────────────────────────────── +const modelDisp = modelName || modelId; +let M = ""; +if (modelDisp) { + M = `${FG_GRAY} ╱ ${FG_BRIGHT_MAGENTA}${I}${modelDisp}${R}`; +} + +// ─── Sandbox Badge ─────────────────────────────────────────────────────────── +let SB = ""; +if (sandboxEnabled || sandboxEnabled === "true") { + if (sandboxNet || sandboxNet === "true") { + SB = `${FG_GRAY}🛡️ sandbox ${FG_BRIGHT_GREEN}${B}ON (net)${R}`; + } else { + SB = `${FG_GRAY}🛡️ sandbox ${FG_BRIGHT_GREEN}${B}ON (no-net)${R}`; + } +} else { + SB = `${FG_GRAY}🛡️ sandbox off${R}`; +} + +// ─── Context Bar (15 segments, fine-grain Unicode) ──────────────────────────── +const barLen = 15; +const pctInt = Math.floor(usedPct); +const filled = Math.floor((pctInt * barLen) / 100); +const remainder = (pctInt * barLen) % 100; + +let barColor = FG_BRIGHT_WHITE; +if (pctInt >= 90) { + barColor = FG_BRIGHT_RED; +} else if (pctInt >= 60) { + barColor = FG_BRIGHT_YELLOW; +} + +let bar = ""; +for (let i = 0; i < barLen; i++) { + if (i < filled) { + bar += "█"; + } else if (i === filled) { + if (remainder >= 75) { + bar += "▓"; + } else if (remainder >= 50) { + bar += "▒"; + } else if (remainder >= 25) { + bar += "░"; + } else { + bar += "·"; + } + } else { + bar += "·"; + } +} + +// ─── Stats & Metadata formatting ───────────────────────────────────────────── +const pctFmt = usedPct.toFixed(1); +const ctxBar = `${FG_GRAY}ctx ${barColor}${bar} ${NUM_COLOR}${pctFmt}%${R}`; +const artFmt = `${FG_GRAY}📦 ${NUM_COLOR}${artifactCount}${R}`; +const subFmt = `${FG_GRAY}🤖 ${NUM_COLOR}${subagentsCount}${R}`; +const bgFmt = `${FG_GRAY}⏳ ${NUM_COLOR}${taskCount}${R}`; + +let dirFmt = ""; +if (cwdShort) { + dirFmt = `${FG_GRAY} ╱ 📂 ${cwdShort}${R}`; +} + +let convFmt = ""; +if (convId) { + convFmt = `${FG_GRAY} ╱ id:${convId.slice(0, 8)}${R}`; +} + +let tokDetails = ""; +if (ctxUsed > 0) { + tokDetails = ` (${humanFormat(ctxUsed)}/${humanFormat(ctxLimit)})`; +} + +const dot = `${FG_GRAY} · ${R}`; + +// ─── Output Assembly ────────────────────────────────────────────────────────── +if (cols >= 120) { + let line1 = `${S}${M}${V}${dirFmt}${convFmt}`; + if (ctxUsed > 0) { + tokDetails = ` (${humanFormat(ctxUsed)}/${humanFormat(ctxLimit)} · ${humanFormat(inputTokens)} in/${humanFormat(outputTokens)} out)`; + } + let line2 = ` ${ctxBar}${tokDetails}${dot}${artFmt}${dot}${subFmt}${dot}${bgFmt}${dot}${SB}`; + console.log(`${line1}${FG_GRAY} │ ${R}${line2}`); +} else if (cols >= 80) { + let line1 = `${S}${M}${V}${dirFmt}`; + let line2 = ` ${ctxBar}${tokDetails}${dot}${artFmt}${dot}${subFmt}${dot}${bgFmt}${dot}${SB}`; + console.log(`${FG_GRAY}╭─${R} ${line1}`); + console.log(`${FG_GRAY}╰─${R}${line2}`); +} else { + let mShort = ""; + if (modelDisp) { + mShort = `${FG_GRAY} ╱ ${FG_BRIGHT_MAGENTA}${modelDisp.slice(0, 12)}${R}`; + } + console.log(`${S}${mShort}`); + console.log(`${ctxBar}${dot}${bgFmt}`); +} diff --git a/examples/statusline/statusline.ps1 b/examples/statusline/statusline.ps1 new file mode 100644 index 000000000..aef6ed3da --- /dev/null +++ b/examples/statusline/statusline.ps1 @@ -0,0 +1,223 @@ +# ─── Read JSON from stdin ──────────────────────────────────────────────────── +$inputJson = [Console]::In.ReadToEnd() +if ([string]::IsNullOrWhiteSpace($inputJson)) { + $inputJson = '{}' +} + +try { + $data = ConvertFrom-Json $inputJson -ErrorAction SilentlyContinue +} catch { + $data = $null +} + +if ($null -eq $data) { + $data = [PSCustomObject]@{} +} + +# ─── ANSI Helpers (Standard 16-color palette only) ─────────────────────────── +$R = "$([char]0x1b)[0m" # Reset +$B = "$([char]0x1b)[1m" # Bold +$D = "$([char]0x1b)[2m" # Dim +$I = "$([char]0x1b)[3m" # Italic + +# Foreground accents (Standard 16 colors) +$FG_BLACK = "$([char]0x1b)[30m" +$FG_RED = "$([char]0x1b)[31m" +$FG_GREEN = "$([char]0x1b)[32m" +$FG_YELLOW = "$([char]0x1b)[33m" +$FG_BLUE = "$([char]0x1b)[34m" +$FG_MAGENTA = "$([char]0x1b)[35m" +$FG_CYAN = "$([char]0x1b)[36m" +$FG_WHITE = "$([char]0x1b)[37m" + +$FG_GRAY = "$([char]0x1b)[90m" +$FG_BRIGHT_RED = "$([char]0x1b)[91m" +$FG_BRIGHT_GREEN = "$([char]0x1b)[92m" +$FG_BRIGHT_YELLOW = "$([char]0x1b)[93m" +$FG_BRIGHT_BLUE = "$([char]0x1b)[94m" +$FG_BRIGHT_MAGENTA = "$([char]0x1b)[95m" +$FG_BRIGHT_CYAN = "$([char]0x1b)[96m" +$FG_BRIGHT_WHITE = "$([char]0x1b)[97m" + +$NUM_COLOR = "${FG_BRIGHT_WHITE}${B}" + +# ─── Extract fields with fallback ──────────────────────────────────────────── +$state = if ($data.agent_state) { $data.agent_state } else { "idle" } +$usedPct = if ($data.context_window -and $null -ne $data.context_window.used_percentage) { [double]$data.context_window.used_percentage } else { 0.0 } +$vcsBranch = if ($data.vcs -and $data.vcs.branch) { $data.vcs.branch } else { "" } +$vcsDirty = if ($data.vcs -and $null -ne $data.vcs.dirty) { $data.vcs.dirty } else { $false } +$vcsType = if ($data.vcs -and $data.vcs.type) { $data.vcs.type } else { "" } +$sandboxEnabled = if ($data.sandbox -and $null -ne $data.sandbox.enabled) { $data.sandbox.enabled } else { $false } +$sandboxNet = if ($data.sandbox -and $null -ne $data.sandbox.allow_network) { $data.sandbox.allow_network } else { $false } +$artifactCount = if ($data.artifact_count) { $data.artifact_count } else { 0 } +$subagentsCount = if ($data.subagents) { @($data.subagents).Count } else { 0 } +$taskCount = if ($data.task_count) { $data.task_count } else { 0 } +$modelId = if ($data.model -and $data.model.id) { $data.model.id } else { "" } +$modelName = if ($data.model -and $data.model.display_name) { $data.model.display_name } else { "" } +$cols = if ($data.terminal_width) { [int]$data.terminal_width } else { 80 } +$cwd = if ($data.cwd) { $data.cwd } else { "" } +$convId = if ($data.conversation_id) { $data.conversation_id } else { "" } +$inputTokens = if ($data.context_window -and $null -ne $data.context_window.total_input_tokens) { $data.context_window.total_input_tokens } else { 0 } +$outputTokens = if ($data.context_window -and $null -ne $data.context_window.total_output_tokens) { $data.context_window.total_output_tokens } else { 0 } +$ctxLimit = if ($data.context_window -and $null -ne $data.context_window.context_window_size) { $data.context_window.context_window_size } else { 0 } +$ctxUsed = if ($data.context_window -and $null -ne $data.context_window.current_usage) { $data.context_window.current_usage } else { 0 } + +# ─── Helper Functions ──────────────────────────────────────────────────────── +function Get-HumanFormat { + param ($num) + if ($null -eq $num -or $num -eq 0) { return "0" } + if ($num -ge 1000000) { + $main = [Math]::Floor($num / 1000000) + $dec = [Math]::Floor(($num % 1000000) / 100000) + return "${main}.${dec}M" + } + if ($num -ge 1000) { + $main = [Math]::Floor($num / 1000) + $dec = [Math]::Floor(($num % 1000) / 100) + return "${main}.${dec}K" + } + return "$num" +} + +function Get-ShortenPath { + param ($path) + if ([string]::IsNullOrEmpty($path)) { return "" } + $homePath = $env:USERPROFILE + if (-not $homePath) { + $homePath = $env:HOME + } + if ($homePath -and $path.StartsWith($homePath)) { + $path = "~" + $path.Substring($homePath.Length) + } + if ($path.Length -gt 25) { + $leaf = Split-Path $path -Leaf + return "...$leaf" + } + return $path +} + +$cwdShort = Get-ShortenPath $cwd + +# ─── State Indicator ────────────────────────────────────────────────────────── +switch ($state) { + "idle" { $S = "${FG_BRIGHT_GREEN}${B}● READY${R}" } + "thinking" { $S = "${FG_BRIGHT_YELLOW}${B}◆ THINKING${R}" } + "working" { $S = "${FG_BRIGHT_CYAN}${B}⚙ WORKING${R}" } + "tool_use" { $S = "${FG_BRIGHT_MAGENTA}${B}🔧 TOOL${R}" } + Default { $S = "${FG_WHITE}${B}⏳ $($state.ToUpper())${R}" } +} + +# ─── VCS Branch & Type ─────────────────────────────────────────────────────── +$V = "" +if (-not [string]::IsNullOrEmpty($vcsBranch)) { + $vcsLabel = if (-not [string]::IsNullOrEmpty($vcsType)) { $vcsType } else { "git" } + if ($vcsDirty -eq $true -or $vcsDirty -eq "true") { + $V = "${FG_GRAY} ╱ ${FG_GRAY}${vcsLabel}:${FG_BRIGHT_RED}${vcsBranch}${FG_BRIGHT_YELLOW}*${R}" + } else { + $V = "${FG_GRAY} ╱ ${FG_GRAY}${vcsLabel}:${FG_BRIGHT_BLUE}${vcsBranch}${R}" + } +} + +# ─── Model ─────────────────────────────────────────────────────────────────── +$modelDisp = if (-not [string]::IsNullOrEmpty($modelName)) { $modelName } else { $modelId } +$M = "" +if (-not [string]::IsNullOrEmpty($modelDisp)) { + $M = "${FG_GRAY} ╱ ${FG_BRIGHT_MAGENTA}${I}${modelDisp}${R}" +} + +# ─── Sandbox Badge ─────────────────────────────────────────────────────────── +if ($sandboxEnabled -eq $true -or $sandboxEnabled -eq "true") { + if ($sandboxNet -eq $true -or $sandboxNet -eq "true") { + $SB = "${FG_GRAY}🛡️ sandbox ${FG_BRIGHT_GREEN}${B}ON (net)${R}" + } else { + $SB = "${FG_GRAY}🛡️ sandbox ${FG_BRIGHT_GREEN}${B}ON (no-net)${R}" + } +} else { + $SB = "${FG_GRAY}🛡️ sandbox off${R}" +} + +# ─── Context Bar ───────────────────────────────────────────────────────────── +$BAR_LEN = 15 +$pctInt = [int][Math]::Floor($usedPct) +$filled = [int][Math]::Floor($pctInt * $BAR_LEN / 100) +$remainder = ($pctInt * $BAR_LEN) % 100 + +$barColor = $FG_BRIGHT_WHITE +if ($pctInt -ge 90) { + $barColor = $FG_BRIGHT_RED +} elseif ($pctInt -ge 60) { + $barColor = $FG_BRIGHT_YELLOW +} + +$BAR = "" +for ($i = 0; $i -lt $BAR_LEN; $i++) { + if ($i -lt $filled) { + $BAR += "█" + } elseif ($i -eq $filled) { + if ($remainder -ge 75) { + $BAR += "▓" + } elseif ($remainder -ge 50) { + $BAR += "▒" + } elseif ($remainder -ge 25) { + $BAR += "░" + } else { + $BAR += "·" + } + } else { + $BAR += "·" + } +} + +# ─── Stats Formatting ──────────────────────────────────────────────────────── +$pctFmt = $usedPct.ToString("0.0", [System.Globalization.CultureInfo]::InvariantCulture) +$CTX_BAR = "${FG_GRAY}ctx ${barColor}${BAR} ${NUM_COLOR}${pctFmt}%${R}" +$ART_FMT = "${FG_GRAY}📦 ${NUM_COLOR}${artifactCount}${R}" +$SUB_FMT = "${FG_GRAY}🤖 ${NUM_COLOR}${subagentsCount}${R}" +$BG_FMT = "${FG_GRAY}⏳ ${NUM_COLOR}${taskCount}${R}" + +$DIR_FMT = "" +if (-not [string]::IsNullOrEmpty($cwdShort)) { + $DIR_FMT = "${FG_GRAY} ╱ 📂 ${cwdShort}${R}" +} + +$CONV_FMT = "" +if (-not [string]::IsNullOrEmpty($convId)) { + $subConvId = if ($convId.Length -gt 8) { $convId.Substring(0, 8) } else { $convId } + $CONV_FMT = "${FG_GRAY} ╱ id:${subConvId}${R}" +} + +$tokDetails = "" +if ($ctxUsed -gt 0) { + $ctxUsedFmt = Get-HumanFormat $ctxUsed + $ctxLimitFmt = Get-HumanFormat $ctxLimit + $tokDetails = " (${ctxUsedFmt}/${ctxLimitFmt})" +} + +$DOT = "${FG_GRAY} · ${R}" + +# ─── Output Assembly ────────────────────────────────────────────────────────── +if ($cols -ge 120) { + $line1 = "${S}${M}${V}${DIR_FMT}${CONV_FMT}" + if ($ctxUsed -gt 0) { + $ctxUsedFmt = Get-HumanFormat $ctxUsed + $ctxLimitFmt = Get-HumanFormat $ctxLimit + $inputTokFmt = Get-HumanFormat $inputTokens + $outputTokFmt = Get-HumanFormat $outputTokens + $tokDetails = " (${ctxUsedFmt}/${ctxLimitFmt} · ${inputTokFmt} in/${outputTokFmt} out)" + } + $line2 = " ${CTX_BAR}${tokDetails}${DOT}${ART_FMT}${DOT}${SUB_FMT}${DOT}${BG_FMT}${DOT}${SB}" + Write-Output "${line1}${FG_GRAY} │ ${R}${line2}" +} elseif ($cols -ge 80) { + $line1 = "${S}${M}${V}${DIR_FMT}" + $line2 = " ${CTX_BAR}${tokDetails}${DOT}${ART_FMT}${DOT}${SUB_FMT}${DOT}${BG_FMT}${DOT}${SB}" + Write-Output "${FG_GRAY}╭─${R} ${line1}" + Write-Output "${FG_GRAY}╰─${R}${line2}" +} else { + $mShort = "" + if (-not [string]::IsNullOrEmpty($modelDisp)) { + $subModelDisp = if ($modelDisp.Length -gt 12) { $modelDisp.Substring(0, 12) } else { $modelDisp } + $mShort = "${FG_GRAY} ╱ ${FG_BRIGHT_MAGENTA}${subModelDisp}${R}" + } + Write-Output "${S}${mShort}" + Write-Output "${CTX_BAR}${DOT}${BG_FMT}" +} diff --git a/examples/title/README.md b/examples/title/README.md index f6da88e67..7070ff1be 100644 --- a/examples/title/README.md +++ b/examples/title/README.md @@ -1,13 +1,15 @@ -# CLI Title Example +# Custom Window Title -This directory contains an example script (`title.sh`) that demonstrates how to dynamically customize the window title for the Antigravity CLI based on the agent's current state. +This directory contains reference implementations for dynamically customizing the terminal window title for the Antigravity CLI across different shell environments: -For more details on how to use and configure the title script, please refer to the official public documentation: -[https://antigravity.google/docs/cli-title](https://antigravity.google/docs/cli-title) +1. **`title.sh`** (Bash/Zsh - Linux/macOS) +2. **`title.js`** (Node.js - Cross-platform: Windows/Linux/macOS) +3. **`title.ps1`** (PowerShell - Windows PowerShell or PowerShell Core `pwsh`) +4. **`title.fish`** (Fish shell - Linux/macOS) ## Quick Start -### Option 1: Automatic Setup (Recommended) +### Option 1: Automatic Setup (Recommended for Bash/Zsh) Run the included setup script from the root of the repository: @@ -21,9 +23,10 @@ This script will automatically: ### Option 2: Manual configuration -1. Copy `title.sh` to a directory of your choice. -2. Edit your `settings.json` file to point `title.command` to the absolute path of `title.sh` and set `title.enabled` to `true`: +1. Copy the script corresponding to your shell to a directory of your choice. +2. Edit your `settings.json` file to point `title.command` to the absolute path of the script and set `title.enabled` to `true`: +#### Bash/Zsh (Linux/macOS) ```json { "title": { @@ -33,6 +36,37 @@ This script will automatically: } ``` +#### Node.js (Cross-platform) +```json +{ + "title": { + "command": "node /absolute/path/to/title.js", + "enabled": true + } +} +``` + +#### PowerShell (Windows / pwsh) +```json +{ + "title": { + "command": "powershell.exe -ExecutionPolicy Bypass -File C:\\absolute\\path\\to\\title.ps1", + "enabled": true + } +} +``` +*For PowerShell Core, use `pwsh.exe` or `pwsh` instead of `powershell.exe`.* + +#### Fish Shell +```json +{ + "title": { + "command": "/absolute/path/to/title.fish", + "enabled": true + } +} +``` + **Settings file locations:** | Platform | Path | @@ -48,12 +82,17 @@ After saving, restart `agy` for changes to take effect. ## How it works -The `title.sh` script reads a JSON payload from standard input, which contains real-time information about the agent's state and context. It then: -1. Extracts the `agent_state` and `workspace.current_dir` using `jq`. -2. Parses the current directory to determine a short workspace name (with special handling for CitC workspaces). +The title script reads a JSON payload from standard input containing the CLI's state. It then: +1. Extracts the `agent_state` and workspace directory path. +2. Parses the current directory to determine a short workspace name. 3. Maps the agent state to a corresponding emoji (e.g., 🤔 for thinking, 🛠️ for tool use, 😴 for idle). 4. Outputs the formatted title string in the format: `[Emoji] [State] | [Workspace]`. +### Prerequisites + +- **Bash (`title.sh`) & Fish (`title.fish`)**: Require [`jq`](https://jqlang.org/) to be installed and available in `$PATH`. +- **Node.js (`title.js`) & PowerShell (`title.ps1`)**: Have **zero external dependencies** and work out-of-the-box. + ## Examples ### Idle State diff --git a/examples/title/title.fish b/examples/title/title.fish new file mode 100755 index 000000000..2090bc13a --- /dev/null +++ b/examples/title/title.fish @@ -0,0 +1,37 @@ +#!/usr/bin/fish + +# Read JSON payload from stdin +set -l DATA (cat) + +if not type -q jq + echo "idle | unknown" + exit 0 +end + +set -l STATE (echo $DATA | jq -r '.agent_state // "idle"') +set -l CWD (echo $DATA | jq -r '.workspace.current_dir // .cwd // ""') + +set -l WORKSPACE "unknown" +if test -n "$CWD" + if string match -r '/google/src/cloud/[^/]+/([^/]+)' $CWD >/dev/null + set WORKSPACE (string replace -r '.*/google/src/cloud/[^/]+/([^/]+).*' '$1' $CWD) + else + set WORKSPACE (basename $CWD) + end +end + +set -l EMOJI "🤖" +switch $STATE + case initializing + set EMOJI "🚀" + case idle + set EMOJI "😴" + case thinking + set EMOJI "🤔" + case working + set EMOJI "🏃" + case tool_use + set EMOJI "🛠️" +end + +echo "$EMOJI $STATE | $WORKSPACE" diff --git a/examples/title/title.js b/examples/title/title.js new file mode 100755 index 000000000..bad3305aa --- /dev/null +++ b/examples/title/title.js @@ -0,0 +1,53 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +// ─── Read JSON from stdin ──────────────────────────────────────────────────── +let rawData = ''; +try { + rawData = fs.readFileSync(0, 'utf-8'); +} catch (e) { + // Silence read errors +} + +let data = {}; +try { + data = JSON.parse(rawData || '{}'); +} catch (e) { + // Silence JSON parse errors +} + +const state = data.agent_state || "idle"; +const cwd = (data.workspace && data.workspace.current_dir) || data.cwd || ""; + +let workspace = "unknown"; +if (cwd) { + const match = cwd.match(/\/google\/src\/cloud\/[^/]+\/([^/]+)/); + if (match) { + workspace = match[1]; + } else { + workspace = path.basename(cwd); + } +} + +// Map state to emoji +let emoji = "🤖"; +switch (state) { + case 'initializing': + emoji = "🚀"; + break; + case 'idle': + emoji = "😴"; + break; + case 'thinking': + emoji = "🤔"; + break; + case 'working': + emoji = "🏃"; + break; + case 'tool_use': + emoji = "🛠️"; + break; +} + +console.log(`${emoji} ${state} | ${workspace}`); diff --git a/examples/title/title.ps1 b/examples/title/title.ps1 new file mode 100644 index 000000000..520c4384b --- /dev/null +++ b/examples/title/title.ps1 @@ -0,0 +1,39 @@ +# ─── Read JSON from stdin ──────────────────────────────────────────────────── +$inputJson = [Console]::In.ReadToEnd() +if ([string]::IsNullOrWhiteSpace($inputJson)) { + $inputJson = '{}' +} + +try { + $data = ConvertFrom-Json $inputJson -ErrorAction SilentlyContinue +} catch { + $data = $null +} + +if ($null -eq $data) { + $data = [PSCustomObject]@{} +} + +$state = if ($data.agent_state) { $data.agent_state } else { "idle" } +$cwd = if ($data.workspace -and $data.workspace.current_dir) { $data.workspace.current_dir } elseif ($data.cwd) { $data.cwd } else { "" } + +$workspace = "unknown" +if (-not [string]::IsNullOrEmpty($cwd)) { + if ($cwd -match '/google/src/cloud/[^/]+/([^/]+)') { + $workspace = $Matches[1] + } else { + $workspace = Split-Path $cwd -Leaf + } +} + +# Map state to emoji +switch ($state) { + "initializing" { $emoji = "🚀" } + "idle" { $emoji = "😴" } + "thinking" { $emoji = "🤔" } + "working" { $emoji = "🏃" } + "tool_use" { $emoji = "🛠️" } + Default { $emoji = "🤖" } +} + +Write-Output "${emoji} ${state} | ${workspace}" From 3724c8e40faedcb0d561ef9fd6e95b478dca00cc Mon Sep 17 00:00:00 2001 From: weby-homelab Date: Thu, 28 May 2026 22:29:55 +0300 Subject: [PATCH 3/3] fix(statusline): correct context_window.current_usage object bug to restore token counts display --- examples/statusline/statusline.fish | 2 +- examples/statusline/statusline.js | 2 +- examples/statusline/statusline.ps1 | 2 +- examples/statusline/statusline.sh | 158 ++++++++++++++++++++++------ 4 files changed, 131 insertions(+), 33 deletions(-) diff --git a/examples/statusline/statusline.fish b/examples/statusline/statusline.fish index cfebf65c6..fae25d7d5 100755 --- a/examples/statusline/statusline.fish +++ b/examples/statusline/statusline.fish @@ -28,7 +28,7 @@ set -l CONV_ID (echo $DATA | jq -r '.conversation_id // ""') set -l INPUT_TOKENS (echo $DATA | jq -r '.context_window.total_input_tokens // 0') set -l OUTPUT_TOKENS (echo $DATA | jq -r '.context_window.total_output_tokens // 0') set -l CTX_LIMIT (echo $DATA | jq -r '.context_window.context_window_size // 0') -set -l CTX_USED (echo $DATA | jq -r '.context_window.current_usage // 0') +set -l CTX_USED (echo $DATA | jq -r '(.context_window.total_input_tokens // 0) + (.context_window.total_output_tokens // 0)') # ANSI Helpers set -l R (set_color normal) diff --git a/examples/statusline/statusline.js b/examples/statusline/statusline.js index 74f812c3c..a8fcc8142 100755 --- a/examples/statusline/statusline.js +++ b/examples/statusline/statusline.js @@ -64,7 +64,7 @@ const convId = data.conversation_id || ""; const inputTokens = (data.context_window && data.context_window.total_input_tokens) ? data.context_window.total_input_tokens : 0; const outputTokens = (data.context_window && data.context_window.total_output_tokens) ? data.context_window.total_output_tokens : 0; const ctxLimit = (data.context_window && data.context_window.context_window_size) ? data.context_window.context_window_size : 0; -const ctxUsed = (data.context_window && data.context_window.current_usage) ? data.context_window.current_usage : 0; +const ctxUsed = inputTokens + outputTokens; // ─── Helper Formatting Functions ───────────────────────────────────────────── function humanFormat(num) { diff --git a/examples/statusline/statusline.ps1 b/examples/statusline/statusline.ps1 index aef6ed3da..e46f8c602 100644 --- a/examples/statusline/statusline.ps1 +++ b/examples/statusline/statusline.ps1 @@ -60,7 +60,7 @@ $convId = if ($data.conversation_id) { $data.conversation_id } else { "" } $inputTokens = if ($data.context_window -and $null -ne $data.context_window.total_input_tokens) { $data.context_window.total_input_tokens } else { 0 } $outputTokens = if ($data.context_window -and $null -ne $data.context_window.total_output_tokens) { $data.context_window.total_output_tokens } else { 0 } $ctxLimit = if ($data.context_window -and $null -ne $data.context_window.context_window_size) { $data.context_window.context_window_size } else { 0 } -$ctxUsed = if ($data.context_window -and $null -ne $data.context_window.current_usage) { $data.context_window.current_usage } else { 0 } +$ctxUsed = $inputTokens + $outputTokens # ─── Helper Functions ──────────────────────────────────────────────────────── function Get-HumanFormat { diff --git a/examples/statusline/statusline.sh b/examples/statusline/statusline.sh index 390fea1ee..893b38fa7 100755 --- a/examples/statusline/statusline.sh +++ b/examples/statusline/statusline.sh @@ -1,5 +1,7 @@ #!/bin/bash set -euo pipefail +INPUT_JSON=$(cat) + # ─── ANSI Helpers (Standard 16-color palette only) ─────────────────────────── R="\033[0m" # Reset @@ -30,38 +32,95 @@ FG_BRIGHT_WHITE="\033[97m" NUM_COLOR="${FG_BRIGHT_WHITE}${B}" # ─── Parse JSON from stdin (Single jq pass for performance) ────────────────── -# Extract all fields in one pass to prevent spawning jq 8 times. { read -r STATE read -r USED_PCT read -r VCS_BRANCH read -r VCS_DIRTY + read -r VCS_TYPE + read -r VCS_CLIENT read -r SANDBOX + read -r SANDBOX_NET read -r ARTIFACTS read -r SUBAGENTS read -r BG_TASKS - read -r MODEL + read -r MODEL_ID + read -r MODEL_NAME read -r COLS + read -r CWD + read -r CONV_ID + read -r PRODUCT + read -r INPUT_TOKENS + read -r OUTPUT_TOKENS + read -r CTX_LIMIT + read -r CTX_USED + read -r REM_PCT } <<< "$( - jq -r ' + echo "$INPUT_JSON" | jq -r ' (.agent_state // "idle"), (.context_window.used_percentage // 0), (.vcs.branch // ""), (.vcs.dirty // false), + (.vcs.type // ""), + (.vcs.client // ""), (.sandbox.enabled // false), + (.sandbox.allow_network // false), (.artifact_count // 0), (if .subagents | type == "array" then (.subagents | length) else 0 end), (.task_count // 0), + (.model.id // ""), (.model.display_name // ""), - (.terminal_width // 80) - ' 2>/dev/null || printf "idle\n0\n\nfalse\nfalse\n0\n0\n0\n\n80\n" + (.terminal_width // 80), + (.cwd // ""), + (.conversation_id // ""), + (.product // ""), + (.context_window.total_input_tokens // 0), + (.context_window.total_output_tokens // 0), + (.context_window.context_window_size // 0), + ((.context_window.total_input_tokens // 0) + (.context_window.total_output_tokens // 0)), + (.context_window.remaining_percentage // 100) + ' 2>/dev/null || printf "idle\n0\n\nfalse\n\n\nfalse\nfalse\n0\n0\n0\n\n\n80\n\n\n\n0\n0\n0\n0\n100\n" )" -# ─── Computed Values ───────────────────────────────────────────────────────── -# Use LC_NUMERIC=C to prevent bash printf errors in locales that use commas for decimals +# ─── Computed & Formatted Values ───────────────────────────────────────────── PCT_FMT=$(LC_NUMERIC=C printf "%.1f" "$USED_PCT") PCT_INT=${USED_PCT%.*}; PCT_INT=${PCT_INT:-0} +human_format() { + local num=$1 + if [ -z "$num" ] || [ "$num" -eq 0 ] 2>/dev/null; then + echo "0" + return + fi + if [ "$num" -ge 1000000 ] 2>/dev/null; then + echo "$((num / 1000000)).$(((num % 1000000) / 100000))M" + elif [ "$num" -ge 1000 ] 2>/dev/null; then + echo "$((num / 1000)).$(((num % 1000) / 100))K" + else + echo "$num" + fi +} + +INPUT_TOK_FMT=$(human_format "$INPUT_TOKENS") +OUTPUT_TOK_FMT=$(human_format "$OUTPUT_TOKENS") +CTX_LIMIT_FMT=$(human_format "$CTX_LIMIT") +CTX_USED_FMT=$(human_format "$CTX_USED") + +shorten_path() { + local path=$1 + if [ -z "$path" ]; then + echo "" + return + fi + path="${path/#$HOME/\~}" + if [ "${#path}" -gt 25 ]; then + echo "...$(basename "$path")" + else + echo "$path" + fi +} +CWD_SHORT=$(shorten_path "$CWD") + # ─── State Indicator (No background colors) ────────────────────────────────── case "$STATE" in idle) S="${FG_BRIGHT_GREEN}${B}● READY${R}" ;; @@ -71,27 +130,34 @@ case "$STATE" in *) S="${FG_WHITE}${B}⏳ $(echo "$STATE" | tr '[:lower:]' '[:upper:]')${R}" ;; esac -# ─── VCS Branch ────────────────────────────────────────────────────────────── +# ─── VCS Branch & Type ─────────────────────────────────────────────────────── V="" if [ -n "$VCS_BRANCH" ]; then + VCS_LABEL="${VCS_TYPE:-git}" if [ "$VCS_DIRTY" = "true" ]; then - V="${FG_GRAY} ╱ ${FG_BRIGHT_RED}${VCS_BRANCH}${FG_BRIGHT_YELLOW}*${R}" + V="${FG_GRAY} ╱ ${FG_GRAY}${VCS_LABEL}:${FG_BRIGHT_RED}${VCS_BRANCH}${FG_BRIGHT_YELLOW}*${R}" else - V="${FG_GRAY} ╱ ${FG_BRIGHT_BLUE}${VCS_BRANCH}${R}" + V="${FG_GRAY} ╱ ${FG_GRAY}${VCS_LABEL}:${FG_BRIGHT_BLUE}${VCS_BRANCH}${R}" fi fi # ─── Model ─────────────────────────────────────────────────────────────────── +# Fallback to model ID if display name is empty +MODEL_DISP="${MODEL_NAME:-$MODEL_ID}" M="" -if [ -n "$MODEL" ]; then - M="${FG_GRAY} ╱ ${FG_BRIGHT_MAGENTA}${I}${MODEL}${R}" +if [ -n "$MODEL_DISP" ]; then + M="${FG_GRAY} ╱ ${FG_BRIGHT_MAGENTA}${I}${MODEL_DISP}${R}" fi # ─── Sandbox Badge ─────────────────────────────────────────────────────────── if [ "$SANDBOX" = "true" ]; then - SB="${FG_GRAY}sandbox ${FG_BRIGHT_GREEN}${B}ON${R}" + if [ "$SANDBOX_NET" = "true" ]; then + SB="${FG_GRAY}🛡️ sandbox ${FG_BRIGHT_GREEN}${B}ON (net)${R}" + else + SB="${FG_GRAY}🛡️ sandbox ${FG_BRIGHT_GREEN}${B}ON (no-net)${R}" + fi else - SB="${FG_GRAY}sandbox off${R}" + SB="${FG_GRAY}🛡️ sandbox off${R}" fi # ─── Context Bar (15 segments, fine-grain Unicode) ──────────────────────────── @@ -99,7 +165,6 @@ BAR_LEN=15 FILLED=$((PCT_INT * BAR_LEN / 100)) REMAINDER=$(( (PCT_INT * BAR_LEN) % 100 )) -# Pick color based on percentage if [ "$PCT_INT" -ge 90 ]; then BAR_COLOR="$FG_BRIGHT_RED" elif [ "$PCT_INT" -ge 60 ]; then @@ -108,7 +173,6 @@ else BAR_COLOR="$FG_BRIGHT_WHITE" fi -# Build bar with partial-fill last block BAR="" for ((i = 0; i < BAR_LEN; i++)); do if [ "$i" -lt "$FILLED" ]; then @@ -128,28 +192,62 @@ for ((i = 0; i < BAR_LEN; i++)); do fi done -# ─── Stats ─────────────────────────────────────────────────────────────────── -CTX="${FG_GRAY}ctx ${BAR_COLOR}${BAR} ${NUM_COLOR}${PCT_FMT}%${R}" -ART_FMT="${FG_GRAY}artifacts ${NUM_COLOR}${ARTIFACTS}${R}" -SUB_FMT="${FG_GRAY}subagents ${NUM_COLOR}${SUBAGENTS}${R}" -BG_FMT="${FG_GRAY}tasks ${NUM_COLOR}${BG_TASKS}${R}" +# ─── Stats & Metadata formatting ───────────────────────────────────────────── +CTX_BAR="${FG_GRAY}ctx ${BAR_COLOR}${BAR} ${NUM_COLOR}${PCT_FMT}%${R}" +ART_FMT="${FG_GRAY}📦 ${NUM_COLOR}${ARTIFACTS}${R}" +SUB_FMT="${FG_GRAY}🤖 ${NUM_COLOR}${SUBAGENTS}${R}" +BG_FMT="${FG_GRAY}⏳ ${NUM_COLOR}${BG_TASKS}${R}" + +# ─── New elements (CWD, Conversation ID, Token counts) ────────────────────── +DIR_FMT="" +if [ -n "$CWD_SHORT" ]; then + DIR_FMT="${FG_GRAY} ╱ 📂 ${CWD_SHORT}${R}" +fi + +CONV_FMT="" +if [ -n "$CONV_ID" ]; then + CONV_FMT="${FG_GRAY} ╱ id:${CONV_ID:0:8}${R}" +fi + +# Token stats detailed vs simple +TOK_DETAILS="" +if [ "$CTX_USED" -gt 0 ] 2>/dev/null; then + TOK_DETAILS=" (${CTX_USED_FMT}/${CTX_LIMIT_FMT})" +fi # ─── Separators ────────────────────────────────────────────────────────────── DOT="${FG_GRAY} · ${R}" -# ─── Output ────────────────────────────────────────────────────────────────── -LINE1="${S}${M}${V}" -LINE2=" ${CTX}${DOT}${ART_FMT}${DOT}${SUB_FMT}${DOT}${BG_FMT}${DOT}${SB}" - +# ─── Output Assembly ────────────────────────────────────────────────────────── if [ "$COLS" -ge 120 ]; then - # Wide: single line + # Wide Layout: One line containing state, model, vcs, directory, conversation id + # and bottom bar metrics inline. + LINE1="${S}${M}${V}${DIR_FMT}${CONV_FMT}" + + # Detailed tokens in wide layout: (used/limit · in/out) + if [ "$CTX_USED" -gt 0 ] 2>/dev/null; then + TOK_DETAILS=" (${CTX_USED_FMT}/${CTX_LIMIT_FMT} · ${INPUT_TOK_FMT} in/${OUTPUT_TOK_FMT} out)" + fi + + LINE2=" ${CTX_BAR}${TOK_DETAILS}${DOT}${ART_FMT}${DOT}${SUB_FMT}${DOT}${BG_FMT}${DOT}${SB}" echo -e "${LINE1}${FG_GRAY} │ ${R}${LINE2}" + elif [ "$COLS" -ge 80 ]; then - # Medium: two-line layout with border + # Medium Layout: Two-line layout with border + LINE1="${S}${M}${V}${DIR_FMT}" + LINE2=" ${CTX_BAR}${TOK_DETAILS}${DOT}${ART_FMT}${DOT}${SUB_FMT}${DOT}${BG_FMT}${DOT}${SB}" + echo -e "${FG_GRAY}╭─${R} ${LINE1}" echo -e "${FG_GRAY}╰─${R}${LINE2}" + else - # Narrow: compact two-line, minimal chrome - echo -e "${S}${M}" - echo -e "${CTX}${DOT}${BG_FMT}" + # Narrow Layout: Compact two-line, minimal layout + # Shorten model display for narrow screens + M_SHORT="" + if [ -n "$MODEL_DISP" ]; then + M_SHORT="${FG_GRAY} ╱ ${FG_BRIGHT_MAGENTA}${MODEL_DISP:0:12}${R}" + fi + + echo -e "${S}${M_SHORT}" + echo -e "${CTX_BAR}${DOT}${BG_FMT}" fi