Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 155 additions & 4 deletions Releases/v4.0.3/.claude/PAI/USER/STATUSLINE/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,158 @@
# Status Line Customization
# Statusline User Extensions

Configure what appears in your Claude Code status line. PAI uses the status line to show session context, active skill, and system state.
Add custom sections to the PAI statusline without modifying `statusline-command.sh`.

## Configuration
## How It Works

Create a `config.md` or modify the `statusline-command.sh` in the `.claude/` root to customize display elements.
Create `PAI/USER/STATUSLINE/extensions.sh` with two functions:

| Function | Called When | Purpose |
|----------|-----------|---------|
| `user_statusline_prefetch $tmp_dir` | Inside the parallel prefetch block | Fetch data, write variables to `$tmp_dir/user-ext.sh` |
| `user_statusline_display` | After all prefetch results are sourced | Render your custom statusline section |

The main statusline script sources your file, calls your functions at the right points, and sources your prefetch output — all without you touching the core script.

## Available Environment

Your extensions run inside the main statusline context. These variables and functions are available:

| Variable/Function | Type | Description |
|-------------------|------|-------------|
| `$MODE` | var | Terminal width mode: `nano`, `micro`, `mini`, `normal` |
| `$USER_TZ` | var | User timezone from settings.json |
| `$PAI_DIR` | var | PAI root directory |
| `$SETTINGS_FILE` | var | Path to settings.json |
| `$RESET` | var | ANSI reset code |
| `$SLATE_500`, `$SLATE_600` | var | Tailwind-inspired color codes |
| `$USAGE_RESET` | var | Muted label color |
| `get_usage_color $pct` | func | Returns ANSI color for 0-100% (green/yellow/red) |
| `get_mtime $file` | func | Cross-platform file modification time (epoch seconds) |

All variables from other prefetch blocks (usage_*, location_*, etc.) are also available in the display function.

## Minimal Example

```bash
#!/bin/bash
# PAI/USER/STATUSLINE/extensions.sh

MY_ICON='\033[38;2;100;200;150m'
MY_LABEL='\033[38;2;130;210;170m'

user_statusline_prefetch() {
local tmp_dir="$1"
# Write any variables your display function needs
echo "my_value=42" > "$tmp_dir/user-ext.sh"
}

user_statusline_display() {
local val=${my_value:-0}
[ "$val" -eq 0 ] && return

case "$MODE" in
nano) printf "${MY_ICON}*${RESET} ${val}\n" ;;
*) printf "${MY_ICON}*${RESET} ${MY_LABEL}CUSTOM:${RESET} ${val}\n" ;;
esac
printf "${SLATE_600}────────────────────────────────────────────────────────────────────────${RESET}\n"
}
```

## Real-World Example: ElevenLabs Voice Quota

Show remaining ElevenLabs TTS characters in the statusline. Only displays when `ELEVENLABS_API_KEY` is set in your `.env`:

```bash
#!/bin/bash
# PAI/USER/STATUSLINE/extensions.sh — ElevenLabs voice usage

ELEVENLABS_CACHE="$PAI_DIR/MEMORY/STATE/elevenlabs-cache.json"
ELEVENLABS_CACHE_TTL=300 # 5 minutes

EL_ICON='\033[38;2;130;100;255m' # Purple
EL_LABEL='\033[38;2;160;130;255m'
EL_VALUE='\033[38;2;200;180;255m'

user_statusline_prefetch() {
local tmp_dir="$1"

# Skip entirely if no API key
if [ -z "${ELEVENLABS_API_KEY:-}" ]; then
echo -e "el_char_used=0\nel_char_limit=0\nel_reset_unix=0" > "$tmp_dir/user-ext.sh"
return
fi

local el_cache_age=999999
[ -f "$ELEVENLABS_CACHE" ] && el_cache_age=$(($(date +%s) - $(get_mtime "$ELEVENLABS_CACHE")))

if [ "$el_cache_age" -gt "$ELEVENLABS_CACHE_TTL" ]; then
local el_json
el_json=$(curl -s --max-time 3 \
-H "xi-api-key: $ELEVENLABS_API_KEY" \
"https://api.elevenlabs.io/v1/user/subscription" 2>/dev/null)

if [ -n "$el_json" ] && echo "$el_json" | jq -e '.character_limit' >/dev/null 2>&1; then
echo "$el_json" | jq '.' > "$ELEVENLABS_CACHE" 2>/dev/null
fi
fi

if [ -f "$ELEVENLABS_CACHE" ]; then
jq -r '
"el_char_used=" + (.character_count // 0 | tostring) + "\n" +
"el_char_limit=" + (.character_limit // 0 | tostring) + "\n" +
"el_reset_unix=" + (.next_character_count_reset_unix // 0 | tostring)
' "$ELEVENLABS_CACHE" > "$tmp_dir/user-ext.sh" 2>/dev/null
else
echo -e "el_char_used=0\nel_char_limit=0\nel_reset_unix=0" > "$tmp_dir/user-ext.sh"
fi
}

user_statusline_display() {
el_char_used=${el_char_used:-0}
el_char_limit=${el_char_limit:-0}
[ "$el_char_limit" -le 0 ] && return

local el_pct=$((el_char_used * 100 / el_char_limit))
local el_remaining=$((el_char_limit - el_char_used))
local el_pct_color
el_pct_color=$(get_usage_color "$el_pct")

local el_remaining_fmt
if [ "$el_remaining" -ge 1000 ]; then
el_remaining_fmt="$(( el_remaining / 1000 )).$(( (el_remaining % 1000) / 100 ))K"
else
el_remaining_fmt="$el_remaining"
fi

case "$MODE" in
nano)
printf "${EL_ICON}♪${RESET} ${el_pct_color}${el_pct}%%${RESET}\n"
;;
micro)
printf "${EL_ICON}♪${RESET} ${EL_LABEL}VOICE:${RESET} ${el_pct_color}${el_pct}%%${RESET} ${EL_VALUE}${el_remaining_fmt} left${RESET}\n"
;;
mini|normal)
printf "${EL_ICON}♪${RESET} ${EL_LABEL}VOICE:${RESET} ${el_pct_color}${el_pct}%%${RESET} ${SLATE_600}│${RESET} ${EL_VALUE}${el_char_used}/${el_char_limit} chars${RESET} ${SLATE_600}│${RESET} ${EL_VALUE}${el_remaining_fmt} left${RESET}\n"
;;
esac
printf "${SLATE_600}────────────────────────────────────────────────────────────────────────${RESET}\n"
}
```

**Output (normal mode):**
```
♪ VOICE: 10% │ 1000/10000 chars │ 9.0K left
────────────────────────────────────────────────────────────────────────
```

## Upgrade Safety

A `SessionStart` hook (`StatuslineExtensions.hook.ts`) automatically checks that the extension wiring is present in `statusline-command.sh`. If an upgrade overwrites the script, the hook re-injects the source line, prefetch call, and display call on next session start. Your `extensions.sh` in `PAI/USER/` is never touched by upgrades.

## Guidelines

- **Gate on missing data.** If your prefetch has no data (missing API key, service down), write zeroed defaults and return early. Your display function should check and produce no output.
- **Respect terminal width.** Use `$MODE` to scale your output. `nano` = minimal, `normal` = full.
- **Cache expensive calls.** Use file-based caching with TTL (see the ElevenLabs example). The prefetch runs on every statusline render.
- **Use the color system.** `get_usage_color` gives consistent green/yellow/red for percentages. Use the existing `$SLATE_*` variables for separators and labels.
- **End with a separator.** Print the `────` line after your section for visual consistency.
109 changes: 109 additions & 0 deletions Releases/v4.0.3/.claude/hooks/StatuslineExtensions.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env bun
/**
* StatuslineExtensions.hook.ts — Ensure user statusline extensions are wired
*
* PURPOSE:
* Self-healing hook that checks if statusline-command.sh has the source line
* for user extensions. If missing (e.g., after a PAI upgrade overwrites the
* script), injects it automatically.
*
* TRIGGER: SessionStart
*
* WHAT IT DOES:
* 1. Reads statusline-command.sh
* 2. Checks for the _USER_EXTENSIONS source block
* 3. If missing and extensions.sh exists, injects:
* - Source line (after .env source)
* - Prefetch call (before parallel block end)
* - user-ext.sh source (after parallel results)
* - Display call (before git/pwd section)
*
* DESIGN: Idempotent — safe to run every session. No-ops if already present
* or if no user extensions file exists.
*/

import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';

const PAI_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude');
const STATUSLINE_PATH = join(PAI_DIR, 'statusline-command.sh');
const EXTENSIONS_PATH = join(PAI_DIR, 'PAI/USER/STATUSLINE/extensions.sh');

const SOURCE_MARKER = '_USER_EXTENSIONS';
const PREFETCH_MARKER = 'user_statusline_prefetch';
const DISPLAY_MARKER = 'user_statusline_display';
const USER_EXT_SOURCE = 'user-ext.sh';

const ENV_SOURCE_LINE = '[ -f "${PAI_CONFIG_DIR:-$HOME/.config/PAI}/.env" ] && source "${PAI_CONFIG_DIR:-$HOME/.config/PAI}/.env"';

function main() {
// No extensions file — nothing to wire
if (!existsSync(EXTENSIONS_PATH)) {
process.exit(0);
}

if (!existsSync(STATUSLINE_PATH)) {
console.error('[StatuslineExtensions] statusline-command.sh not found');
process.exit(0);
}

let content = readFileSync(STATUSLINE_PATH, 'utf-8');
let modified = false;

// 1. Check for source line
if (!content.includes(SOURCE_MARKER)) {
const envIdx = content.indexOf(ENV_SOURCE_LINE);
if (envIdx === -1) {
console.error('[StatuslineExtensions] Could not find .env source line to anchor injection');
process.exit(0);
}
const insertAfter = envIdx + ENV_SOURCE_LINE.length;
const injection = `\n\n# Source user statusline extensions (upgrade-safe customizations)\n_USER_EXTENSIONS="$PAI_DIR/PAI/USER/STATUSLINE/extensions.sh"\n[ -f "$_USER_EXTENSIONS" ] && source "$_USER_EXTENSIONS"`;
content = content.slice(0, insertAfter) + injection + content.slice(insertAfter);
modified = true;
Comment on lines +37 to +63
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook anchors injection by searching for an exact, full-string match of the .env source line. Any small formatting change in statusline-command.sh (spacing, quoting, variable name, comment) will cause the hook to fail to inject and silently exit. Consider matching more robustly (e.g., regex for sourcing the .env file, or anchoring on a nearby comment block) so the “self-healing” behavior survives upstream script edits.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont know about the system enough to know what to do here

console.error('[StatuslineExtensions] Injected extensions source line');
}

// 2. Check for prefetch call in parallel block
if (!content.includes(PREFETCH_MARKER)) {
const parallelEnd = content.indexOf('# --- PARALLEL BLOCK END');
if (parallelEnd !== -1) {
const injection = `# User extensions prefetch\n{ type -t user_statusline_prefetch &>/dev/null && user_statusline_prefetch "$_parallel_tmp"; } &\n\n`;
content = content.slice(0, parallelEnd) + injection + content.slice(parallelEnd);
modified = true;
console.error('[StatuslineExtensions] Injected prefetch call');
}
}

// 3. Check for user-ext.sh source in parallel results
if (!content.includes(USER_EXT_SOURCE)) {
const lastSource = content.lastIndexOf('" ] && source "$_parallel_tmp/');
if (lastSource !== -1) {
const lineEnd = content.indexOf('\n', lastSource);
const injection = `\n[ -f "$_parallel_tmp/user-ext.sh" ] && source "$_parallel_tmp/user-ext.sh"`;
content = content.slice(0, lineEnd) + injection + content.slice(lineEnd);
modified = true;
console.error('[StatuslineExtensions] Injected user-ext.sh source');
}
Comment on lines +78 to +87
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user-ext.sh source injection relies on lastIndexOf('" ] && source "$_parallel_tmp/') to find the insertion point. This is brittle to minor formatting changes in statusline-command.sh and could insert in the wrong place or not at all. Prefer a more stable anchor (e.g., insert before the rm -rf "$_parallel_tmp" line or after a named marker comment) and/or use a regex that matches the parallel results block structure.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i honestly dont know anything about this so dont know how to move forward

}

// 4. Check for display call
if (!content.includes(DISPLAY_MARKER)) {
const gitLine = content.indexOf('# LINE 4: PWD & GIT');
if (gitLine !== -1) {
const injection = `# ═══════════════════════════════════════════════════════════════════════════════\n# LINE: USER EXTENSIONS DISPLAY\n# ═══════════════════════════════════════════════════════════════════════════════\ntype -t user_statusline_display &>/dev/null && user_statusline_display\n\n`;
content = content.slice(0, gitLine) + injection + content.slice(gitLine);
modified = true;
console.error('[StatuslineExtensions] Injected display call');
}
}

if (modified) {
writeFileSync(STATUSLINE_PATH, content);
console.error('[StatuslineExtensions] statusline-command.sh patched successfully');
}

process.exit(0);
}

main();
4 changes: 4 additions & 0 deletions Releases/v4.0.3/.claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@
{
"type": "command",
"command": "bun ${PAI_DIR}/hooks/handlers/BuildCLAUDE.ts"
},
{
"type": "command",
"command": "bun ${PAI_DIR}/hooks/StatuslineExtensions.hook.ts"
}
]
}
Expand Down
15 changes: 15 additions & 0 deletions Releases/v4.0.3/.claude/statusline-command.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ COUNTS_CACHE="$PAI_DIR/MEMORY/STATE/counts-cache.sh"
# Source .env for API keys
[ -f "${PAI_CONFIG_DIR:-$HOME/.config/PAI}/.env" ] && source "${PAI_CONFIG_DIR:-$HOME/.config/PAI}/.env"

# Source user statusline extensions (upgrade-safe customizations)
_USER_EXTENSIONS="$PAI_DIR/PAI/USER/STATUSLINE/extensions.sh"
[ -f "$_USER_EXTENSIONS" ] && source "$_USER_EXTENSIONS"

# Cross-platform file mtime (seconds since epoch)
# macOS uses stat -f %m, Linux uses stat -c %Y
get_mtime() {
Expand Down Expand Up @@ -396,6 +400,11 @@ COUNTSEOF
fi
} &

# User extensions prefetch
if type -t user_statusline_prefetch &>/dev/null; then
user_statusline_prefetch "$_parallel_tmp" &
fi

# --- PARALLEL BLOCK END - wait for all to complete ---
wait

Expand All @@ -405,6 +414,7 @@ wait
[ -f "$_parallel_tmp/weather.sh" ] && source "$_parallel_tmp/weather.sh"
[ -f "$_parallel_tmp/counts.sh" ] && source "$_parallel_tmp/counts.sh"
[ -f "$_parallel_tmp/usage.sh" ] && source "$_parallel_tmp/usage.sh"
[ -f "$_parallel_tmp/user-ext.sh" ] && source "$_parallel_tmp/user-ext.sh"
rm -rf "$_parallel_tmp" 2>/dev/null

learning_count="$learnings_count"
Expand Down Expand Up @@ -1021,6 +1031,11 @@ print(f\"clock_7d='{clock_time(r7d, 'weekly')}'\")
printf "${SLATE_600}────────────────────────────────────────────────────────────────────────${RESET}\n"
fi

# ═══════════════════════════════════════════════════════════════════════════════
# LINE: USER EXTENSIONS DISPLAY
# ═══════════════════════════════════════════════════════════════════════════════
type -t user_statusline_display &>/dev/null && user_statusline_display

# ═══════════════════════════════════════════════════════════════════════════════
# LINE 4: PWD & GIT (index-only: branch, age, stash, sync — no file status)
# ═══════════════════════════════════════════════════════════════════════════════
Expand Down
Loading