diff --git a/examples/statusline/statusline.ps1 b/examples/statusline/statusline.ps1 new file mode 100644 index 000000000..4e540e844 --- /dev/null +++ b/examples/statusline/statusline.ps1 @@ -0,0 +1,389 @@ +# ─── Read JSON from stdin ──────────────────────────────────────────────────── +param( + [Parameter(ValueFromPipeline)] + [string]$inputJson +) + +if ([string]::IsNullOrWhiteSpace($inputJson)) { + # Try reading from Console stdin (for redirected stdin from Go/agy.exe) + $inputJson = [Console]::In.ReadToEnd() +} + +if ([string]::IsNullOrWhiteSpace($inputJson)) { + $inputJson = '{}' +} + +try { + # -ErrorAction Stop is needed so catch block is triggered on invalid JSON + $data = ConvertFrom-Json $inputJson -ErrorAction Stop +} catch { + # Output the JSON parsing error in red to make debugging copy-pasted blocks easy + Write-Output "$([char]0x1b)[31m● ERROR: Invalid JSON input to statusline.ps1 ($($_.Exception.Message))$([char]0x1b)[0m" + exit +} + +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 = $inputTokens + $outputTokens + +$gemini5h = if ($data.quota -and $data.quota.'gemini-5h' -and $null -ne $data.quota.'gemini-5h'.remaining_fraction) { [double]$data.quota.'gemini-5h'.remaining_fraction * 100 } else { -1.0 } +$geminiWk = if ($data.quota -and $data.quota.'gemini-weekly' -and $null -ne $data.quota.'gemini-weekly'.remaining_fraction) { [double]$data.quota.'gemini-weekly'.remaining_fraction * 100 } else { -1.0 } +$tp5h = if ($data.quota -and $data.quota.'3p-5h' -and $null -ne $data.quota.'3p-5h'.remaining_fraction) { [double]$data.quota.'3p-5h'.remaining_fraction * 100 } else { -1.0 } +$tpWk = if ($data.quota -and $data.quota.'3p-weekly' -and $null -ne $data.quota.'3p-weekly'.remaining_fraction) { [double]$data.quota.'3p-weekly'.remaining_fraction * 100 } else { -1.0 } + +$gemini5hReset = if ($data.quota -and $data.quota.'gemini-5h' -and $null -ne $data.quota.'gemini-5h'.reset_in_seconds) { [int]$data.quota.'gemini-5h'.reset_in_seconds } else { -1 } +$geminiWkReset = if ($data.quota -and $data.quota.'gemini-weekly' -and $null -ne $data.quota.'gemini-weekly'.reset_in_seconds) { [int]$data.quota.'gemini-weekly'.reset_in_seconds } else { -1 } +$tp5hReset = if ($data.quota -and $data.quota.'3p-5h' -and $null -ne $data.quota.'3p-5h'.reset_in_seconds) { [int]$data.quota.'3p-5h'.reset_in_seconds } else { -1 } +$tpWkReset = if ($data.quota -and $data.quota.'3p-weekly' -and $null -ne $data.quota.'3p-weekly'.reset_in_seconds) { [int]$data.quota.'3p-weekly'.reset_in_seconds } else { -1 } + +# ─── VCS directly from git (bypasses JSON parsing entirely for accuracy) ────── +try { + $gitDir = if ($cwd) { $cwd } else { "." } + $branch = & git -C "$gitDir" rev-parse --abbrev-ref HEAD 2>$null + if ($branch) { + $vcsBranch = $branch.Trim() + $vcsType = "git" + $status = & git -C "$gitDir" status --porcelain 2>$null + $vcsDirty = if ($status) { $true } else { $false } + } +} catch { + # Ignore git errors +} + +# ─── 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 +} + +function Get-VisibleLen { + param ($str) + $stripped = $str -replace '\x1b\[[0-9;]*m', '' + return $stripped.Length +} + +function Write-RightAligned { + param ($left, $right, $totalCols) + $leftVis = Get-VisibleLen $left + $rightVis = Get-VisibleLen $right + $pad = $totalCols - $leftVis - $rightVis + if ($pad -lt 1) { $pad = 1 } + $spaces = " " * $pad + Write-Output "${left}${spaces}${right}" +} + +$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}" } +} + +# ─── Separators ────────────────────────────────────────────────────────────── +$DOT = "${FG_GRAY} | ${R}" + +# ─── VCS Branch & Type ─────────────────────────────────────────────────────── +$V = "" +if (-not [string]::IsNullOrEmpty($vcsBranch)) { + if ($vcsDirty -eq $true -or $vcsDirty -eq "true") { + $V = "${DOT}${R}${FG_BRIGHT_RED} ${vcsBranch}${FG_BRIGHT_YELLOW}*${R}" + } else { + $V = "${DOT}${R}${FG_BRIGHT_BLUE} ${vcsBranch}${R}" + } +} + +# ─── Model ─────────────────────────────────────────────────────────────────── +$modelDisp = if (-not [string]::IsNullOrEmpty($modelName)) { $modelName } else { $modelId } +$M = "" +if (-not [string]::IsNullOrEmpty($modelDisp)) { + $M = "${FG_GRAY}${DOT}${FG_BRIGHT_MAGENTA}${I} ${R}${modelDisp}${R}" +} + +# ─── Sandbox Badge ─────────────────────────────────────────────────────────── +if ($sandboxEnabled -eq $true -or $sandboxEnabled -eq "true") { + if ($sandboxNet -eq $true -or $sandboxNet -eq "true") { + $SB = "${FG_GREEN}󰒙 ${FG_BRIGHT_GREEN}${B}ON (net)${R}" + } else { + $SB = "${FG_GREEN}󰴴 ${FG_BRIGHT_GREEN}${B}ON (no-net)${R}" + } +} else { + $SB = "${FG_RED}󰦜 ${FG_BRIGHT_RED}${B}OFF${R}" +} + +# ─── Context Bar (20 segments) ─────────────────────────────────────────────── +$BAR_LEN = 20 +$pctInt = [int][Math]::Floor($usedPct) +$filled = [int][Math]::Floor($pctInt * $BAR_LEN / 100) +$remainder = ($pctInt * $BAR_LEN) % 100 + +$barColor = $FG_YELLOW +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_YELLOW}󱍏 ${R}${barColor}${BAR}${R} ${NUM_COLOR}${pctFmt}%${R}" +$ART_FMT = "${FG_BLUE} ${NUM_COLOR}${artifactCount}${R}" +$SUB_FMT = "${FG_CYAN}󱙺 ${NUM_COLOR}${subagentsCount}${R}" +$BG_FMT = "${FG_MAGENTA} ${NUM_COLOR}${taskCount}${R}" + +$DIR_FMT = "" +if (-not [string]::IsNullOrEmpty($cwdShort)) { + $DIR_FMT = "${FG_GRAY}${DOT}${FG_CYAN} ${R}${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}${DOT}${FG_GRAY}󰍪 ${subConvId}${R}" +} + +$tokDetails = "" +if ($ctxUsed -gt 0) { + $ctxUsedFmt = Get-HumanFormat $ctxUsed + $ctxLimitFmt = Get-HumanFormat $ctxLimit + $tokDetails = " (${ctxUsedFmt}/${ctxLimitFmt})" +} + +# ─── Quota formatting ──────────────────────────────────────────────────────── +function Get-ResetTimeFmt { + param ($sec) + if ($null -eq $sec -or $sec -le 0) { return "" } + $days = [Math]::Floor($sec / 86400) + $rem = $sec % 86400 + $hours = [Math]::Floor($rem / 3600) + $mins = [Math]::Floor(($rem % 3600) / 60) + + if ($days -gt 0) { + if ($hours -gt 0) { return "${days}d ${hours}h" } + return "${days}d" + } elseif ($hours -gt 0) { + if ($mins -gt 0) { return "${hours}h ${mins}m" } + return "${hours}h" + } elseif ($mins -gt 0) { + return "${mins}m" + } else { + return "<1m" + } +} + +function Get-QuotaBar { + param ($val, $label, $barColor, $resetSec) + if ($null -eq $val -or $val -lt 0) { + $emptyBar = "░" * 20 + return "${FG_GRAY}| ${R}${FG_BRIGHT_WHITE}${B}${label}${R} ${FG_GRAY}${emptyBar} N/A${R}" + } + $color = $FG_BRIGHT_GREEN + if ($val -lt 20) { + $color = $FG_BRIGHT_RED + } elseif ($val -lt 50) { + $color = $FG_BRIGHT_YELLOW + } + + $barLen = 20 + $pctInt = [int][Math]::Floor($val) + $filled = [int][Math]::Floor($pctInt * $barLen / 100) + $remainder = ($pctInt * $barLen) % 100 + + $bar = "" + for ($i = 0; $i -lt $barLen; $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 += "░" + } + } + + $coloredBar = "" + $inColor = $false + for ($i = 0; $i -lt $barLen; $i++) { + $char = $bar[$i] + if ($char -eq "█" -or $char -eq "▓" -or $char -eq "▒") { + if (-not $inColor) { + $coloredBar += $barColor + $inColor = $true + } + $coloredBar += $char + } else { + if ($inColor) { + $coloredBar += $R + $inColor = $false + } + $coloredBar += "${FG_GRAY}${char}${R}" + } + } + if ($inColor) { + $coloredBar += $R + } + + $valFmt = $val.ToString("0.0", [System.Globalization.CultureInfo]::InvariantCulture) + if ($valFmt.EndsWith(".0")) { + $valFmt = $valFmt.Substring(0, $valFmt.Length - 2) + } + + $resetStr = "" + if ($null -ne $resetSec -and $resetSec -gt 0) { + $timeFmt = Get-ResetTimeFmt -sec $resetSec + $resetStr = " ⌛️ ${timeFmt}" + } + return "${FG_GRAY}| ${R}${FG_BRIGHT_WHITE}${B}${label}${R} ${coloredBar} ${color}${valFmt}%${R}${resetStr}" +} + +$isGemini = $modelDisp.ToLower().Contains("gemini") +if ($isGemini) { + $q5h = $gemini5h + $qWk = $geminiWk + $q5hR = $gemini5hReset + $qWkR = $geminiWkReset +} else { + $q5h = $tp5h + $qWk = $tpWk + $q5hR = $tp5hReset + $qWkR = $tpWkReset +} + +$QUOTA_FMT = "" +if ($q5h -ge 0 -or $qWk -ge 0) { + $fmt5h = Get-QuotaBar -val $q5h -label "5H" -barColor $FG_BRIGHT_CYAN -resetSec $q5hR + $fmtWk = Get-QuotaBar -val $qWk -label "7D" -barColor $FG_BRIGHT_MAGENTA -resetSec $qWkR + $QUOTA_FMT = "${fmt5h} ${fmtWk}" +} + +# ─── Output Assembly ────────────────────────────────────────────────────────── +if ($cols -ge 180) { + $line1 = "${S}${M}${DIR_FMT}${V}${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})${DOT}${FG_YELLOW} ${R} (${inputTokFmt} in/${outputTokFmt} out)" + } + $line2 = "${ART_FMT}${DOT}${SUB_FMT}${DOT}${BG_FMT}${DOT}${SB}${DOT}${CTX_BAR}${tokDetails}${DOT}${QUOTA_FMT}" + Write-RightAligned -left $line1 -right $line2 -totalCols $cols +} elseif ($cols -ge 90) { + $line1 = "${S}${M}${DIR_FMT}${V}" + $line2 = " ${CTX_BAR}${tokDetails}${DOT}${ART_FMT}${DOT}${SUB_FMT}${DOT}${BG_FMT}${DOT}${SB}${DOT}${QUOTA_FMT}" + 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}${DOT}${QUOTA_FMT}" +} diff --git a/examples/statusline/statusline.sh b/examples/statusline/statusline.sh index 390fea1ee..16592b7d4 100755 --- a/examples/statusline/statusline.sh +++ b/examples/statusline/statusline.sh @@ -1,5 +1,11 @@ #!/bin/bash set -euo pipefail +if [ -t 0 ]; then + INPUT_JSON="{}" +else + INPUT_JSON=$(cat) +fi + # ─── ANSI Helpers (Standard 16-color palette only) ─────────────────────────── R="\033[0m" # Reset @@ -30,126 +36,369 @@ 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 + read -r GEMINI_5H + read -r GEMINI_WK + read -r TP_5H + read -r TP_WK + read -r GEMINI_5H_RESET + read -r GEMINI_WK_RESET + read -r TP_5H_RESET + read -r TP_WK_RESET } <<< "$( - 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), + (if .quota["gemini-5h"].remaining_fraction != null then ((.quota["gemini-5h"].remaining_fraction * 1000 | round) / 10) else -1 end), + (if .quota["gemini-weekly"].remaining_fraction != null then ((.quota["gemini-weekly"].remaining_fraction * 1000 | round) / 10) else -1 end), + (if .quota["3p-5h"].remaining_fraction != null then ((.quota["3p-5h"].remaining_fraction * 1000 | round) / 10) else -1 end), + (if .quota["3p-weekly"].remaining_fraction != null then ((.quota["3p-weekly"].remaining_fraction * 1000 | round) / 10) else -1 end), + (.quota["gemini-5h"].reset_in_seconds // -1), + (.quota["gemini-weekly"].reset_in_seconds // -1), + (.quota["3p-5h"].reset_in_seconds // -1), + (.quota["3p-weekly"].reset_in_seconds // -1) + ' 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-1\n-1\n-1\n-1\n-1\n-1\n-1\n-1\n" )" -# ─── Computed Values ───────────────────────────────────────────────────────── -# Use LC_NUMERIC=C to prevent bash printf errors in locales that use commas for decimals + +# ─── Separators ────────────────────────────────────────────────────────────── +DOT="${FG_GRAY} | ${R}" + + +# ─── VCS directly from git (bypasses JSON parsing entirely) ────────────────── +GIT_DIR="${CWD:-.}" +VCS_BRANCH=$(git -C "$GIT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "") +if [ -n "$VCS_BRANCH" ]; then + VCS_TYPE="git" + if git -C "$GIT_DIR" status --porcelain 2>/dev/null | grep -q .; then + VCS_DIRTY="true" + else + VCS_DIRTY="false" + fi +else + VCS_TYPE="" + VCS_DIRTY="false" +fi + +# ─── Computed & Formatted Values ───────────────────────────────────────────── PCT_FMT=$(LC_NUMERIC=C printf "%.1f" "$USED_PCT") PCT_INT=${USED_PCT%.*}; PCT_INT=${PCT_INT:-0} -# ─── State Indicator (No background colors) ────────────────────────────────── +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") + +# ─── Strip ANSI escapes to measure visible length ──────────────────────────── +visible_len() { + # Strips ESC sequences and counts remaining bytes + printf '%s' "$(echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g')" | wc -m +} + +# ─── State Indicator ───────────────────────────────────────────────────────── case "$STATE" in - 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}" ;; - *) S="${FG_WHITE}${B}⏳ $(echo "$STATE" | tr '[:lower:]' '[:upper:]')${R}" ;; + 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}" ;; + *) S="${FG_WHITE}${B}  $(echo "$STATE" | tr '[:lower:]' '[:upper:]')${R}" ;; esac -# ─── VCS Branch ────────────────────────────────────────────────────────────── +# ─── VCS Branch & Type (fixed: color applied correctly in both branches) ───── 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="${DOT}${R}${FG_BRIGHT_RED} ${VCS_BRANCH}${FG_BRIGHT_YELLOW}*${R}" else - V="${FG_GRAY} ╱ ${FG_BRIGHT_BLUE}${VCS_BRANCH}${R}" + V="${DOT}${R}${FG_BRIGHT_BLUE} ${VCS_BRANCH}${R}" fi fi # ─── Model ─────────────────────────────────────────────────────────────────── +MODEL_DISP=" ${R}${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}${DOT}${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_GREEN}󰒙 ${FG_BRIGHT_GREEN}${B}ON (net)${R}" + else + SB="${FG_GREEN}󰴴 ${FG_BRIGHT_GREEN}${B}ON (no-net)${R}" + fi else - SB="${FG_GRAY}sandbox off${R}" + SB="${FG_RED}󰦜 ${FG_BRIGHT_RED}${B}OFF${R}" fi -# ─── Context Bar (15 segments, fine-grain Unicode) ──────────────────────────── -BAR_LEN=15 +# ─── Context Bar (20 segments) ─────────────────────────────────────────────── +BAR_LEN=20 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 - BAR_COLOR="$FG_BRIGHT_YELLOW" -else - BAR_COLOR="$FG_BRIGHT_WHITE" +if [ "$PCT_INT" -ge 90 ]; then FILL_COLOR="$FG_BRIGHT_RED" +elif [ "$PCT_INT" -ge 60 ]; then FILL_COLOR="$FG_BRIGHT_YELLOW" +else FILL_COLOR="$FG_YELLOW" fi -# Build bar with partial-fill last block BAR="" for ((i = 0; i < BAR_LEN; i++)); do - if [ "$i" -lt "$FILLED" ]; then - BAR="${BAR}█" + if [ "$i" -lt "$FILLED" ]; then + BAR="${BAR}${FILL_COLOR}█${R}" elif [ "$i" -eq "$FILLED" ]; then - if [ "$REMAINDER" -ge 75 ]; then - BAR="${BAR}▓" - elif [ "$REMAINDER" -ge 50 ]; then - BAR="${BAR}▒" - elif [ "$REMAINDER" -ge 25 ]; then - BAR="${BAR}░" + if [ "$REMAINDER" -ge 75 ]; then BAR="${BAR}${FILL_COLOR}▓${R}${FG_GRAY}" + elif [ "$REMAINDER" -ge 50 ]; then BAR="${BAR}${FILL_COLOR}▒${R}${FG_GRAY}" + else BAR="${BAR}${FILL_COLOR}░${R}${FG_GRAY}" + fi + else BAR="${BAR}${FG_GRAY}░${R}" + fi +done + +# ─── Stats & Metadata formatting ───────────────────────────────────────────── +CTX_BAR="${FG_YELLOW}󱍏 ${R}${BAR} ${NUM_COLOR}${PCT_FMT}%${R}" +ART_FMT="${FG_BLUE} ${NUM_COLOR}${ARTIFACTS}${R}" +SUB_FMT="${FG_CYAN}󱙺 ${NUM_COLOR}${SUBAGENTS}${R}" +BG_FMT="${FG_MAGENTA} ${NUM_COLOR}${BG_TASKS}${R}" + +DIR_FMT="" +if [ -n "$CWD_SHORT" ]; then + DIR_FMT="${FG_GRAY}${DOT}${FG_CYAN} ${R}${CWD_SHORT}${R}" +fi + +CONV_FMT="" +if [ -n "$CONV_ID" ]; then + CONV_FMT="${FG_GRAY}${DOT}${FG_GRAY}󰍪 ${CONV_ID:0:8}${R}" +fi + +TOK_DETAILS="" +if [ "$CTX_USED" -gt 0 ] 2>/dev/null; then + TOK_DETAILS=" (${CTX_USED_FMT}/${CTX_LIMIT_FMT})" +fi + +# ─── Quota formatting ──────────────────────────────────────────────────────── +format_reset_time() { + local sec=$1 + if [ -z "$sec" ] || [ "$sec" -le 0 ]; then + echo -n "" + return + fi + + local days=$((sec / 86400)) + local rem=$((sec % 86400)) + local hours=$((rem / 3600)) + rem=$((rem % 3600)) + local mins=$((rem / 60)) + + if [ "$days" -gt 0 ]; then + if [ "$hours" -gt 0 ]; then + echo -n "${days}d ${hours}h" else - BAR="${BAR}·" + echo -n "${days}d" fi + elif [ "$hours" -gt 0 ]; then + if [ "$mins" -gt 0 ]; then + echo -n "${hours}h ${mins}m" + else + echo -n "${hours}h" + fi + elif [ "$mins" -gt 0 ]; then + echo -n "${mins}m" else - BAR="${BAR}·" + echo -n "<1m" + fi +} + +make_quota_bar() { + local val=$1 + local label=$2 + local bar_color=$3 + local reset_sec=$4 + if [ -z "$val" ] || [[ "$val" == -* ]]; then + local bar="" + for ((i = 0; i < 20; i++)); do + bar="${bar}░" + done + echo -n "${FG_GRAY}| ${R}${FG_BRIGHT_WHITE}${B}${label}${R} ${FG_GRAY}${bar} N/A${R}" + return 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}" + local val_int=${val%.*} + val_int=${val_int:-0} + + local text_color="$FG_BRIGHT_GREEN" + if [ "$val_int" -lt 20 ]; then + text_color="$FG_BRIGHT_RED" + elif [ "$val_int" -lt 50 ]; then + text_color="$FG_BRIGHT_YELLOW" + fi -# ─── Separators ────────────────────────────────────────────────────────────── -DOT="${FG_GRAY} · ${R}" - -# ─── Output ────────────────────────────────────────────────────────────────── -LINE1="${S}${M}${V}" -LINE2=" ${CTX}${DOT}${ART_FMT}${DOT}${SUB_FMT}${DOT}${BG_FMT}${DOT}${SB}" - -if [ "$COLS" -ge 120 ]; then - # Wide: single line - echo -e "${LINE1}${FG_GRAY} │ ${R}${LINE2}" -elif [ "$COLS" -ge 80 ]; then - # Medium: two-line layout with border - echo -e "${FG_GRAY}╭─${R} ${LINE1}" + local bar_len=20 + local filled=$((val_int * bar_len / 100)) + local remainder=$(( (val_int * bar_len) % 100 )) + + local bar="" + for ((i = 0; i < bar_len; i++)); do + if [ "$i" -lt "$filled" ]; then + bar="${bar}${bar_color}█${R}" + elif [ "$i" -eq "$filled" ]; then + if [ "$remainder" -ge 75 ]; then + bar="${bar}${bar_color}▓${R}${FG_GRAY}" + elif [ "$remainder" -ge 50 ]; then + bar="${bar}${bar_color}▒${R}${FG_GRAY}" + elif [ "$remainder" -ge 25 ]; then + bar="${bar}${bar_color}░${R}${FG_GRAY}" + else + bar="${bar}${FG_GRAY}░${R}" + fi + else + bar="${bar}${FG_GRAY}░${R}" + fi + done + + local reset_str="" + if [ -n "$reset_sec" ] && [ "$reset_sec" -gt 0 ]; then + reset_str=" ⌛️ $(format_reset_time "$reset_sec")" + fi + + echo -n "${FG_GRAY}| ${R}${FG_BRIGHT_WHITE}${B}${label}${R} ${bar} ${text_color}${val}%${R}${reset_str}" +} + +# Determine active quota +MODEL_LOWER=$(echo "${MODEL_NAME:-$MODEL_ID}" | tr '[:upper:]' '[:lower:]') +if [[ "$MODEL_LOWER" == *gemini* ]]; then + Q_5H="$GEMINI_5H" + Q_WK="$GEMINI_WK" + Q_5H_R="$GEMINI_5H_RESET" + Q_WK_R="$GEMINI_WK_RESET" +else + Q_5H="$TP_5H" + Q_WK="$TP_WK" + Q_5H_R="$TP_5H_RESET" + Q_WK_R="$TP_WK_RESET" +fi + +QUOTA_FMT="" +if [ -n "$Q_5H" ] || [ -n "$Q_WK" ]; then + QUOTA_FMT="$(make_quota_bar "$Q_5H" "5H" "$FG_BRIGHT_CYAN" "$Q_5H_R") $(make_quota_bar "$Q_WK" "7D" "$FG_BRIGHT_MAGENTA" "$Q_WK_R")" +fi + +# ─── Right-align helper ────────────────────────────────────────────────────── +# Prints LINE1 left-aligned and LINE2 right-aligned on the same terminal row. +print_right_aligned() { + local left="$1" + local right="$2" + local total_cols="$3" + + local left_vis right_vis pad + left_vis=$(visible_len "$left") + right_vis=$(visible_len "$right") + + # How many spaces needed between left and right + pad=$(( total_cols - left_vis - right_vis )) + [ "$pad" -lt 1 ] && pad=1 + + printf "%b%*s%b\n" "$left" "$pad" "" "$right" +} + +# ─── Output Assembly ───────────────────────────────────────────────────────── +if [ "$COLS" -ge 180 ]; then + LINE1="${S}${M}${DIR_FMT}${V}${CONV_FMT}" + + if [ "$CTX_USED" -gt 0 ] 2>/dev/null; then + TOK_DETAILS=" (${CTX_USED_FMT}/${CTX_LIMIT_FMT})${DOT}${FG_YELLOW} ${R} (${INPUT_TOK_FMT} in/${OUTPUT_TOK_FMT} out)" + fi + + LINE2="${ART_FMT}${DOT}${SUB_FMT}${DOT}${BG_FMT}${DOT}${SB}${DOT}${CTX_BAR}${TOK_DETAILS}${DOT}${QUOTA_FMT}" + print_right_aligned "$LINE1" "$LINE2" "$COLS" + +elif [ "$COLS" -ge 90 ]; then + # Medium Layout: Two-line layout with border + LINE1="${S}${M}${DIR_FMT}${V}" + LINE2=" ${CTX_BAR}${TOK_DETAILS}${DOT}${ART_FMT}${DOT}${SUB_FMT}${DOT}${BG_FMT}${DOT}${SB}${DOT}${QUOTA_FMT}" + + 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}" + M_SHORT="" + if [ -n "$MODEL_DISP" ]; then + M_SHORT="${FG_GRAY} ╱ ${FG_BRIGHT_MAGENTA}${MODEL_DISP}${R}" + fi + + echo -e "${S}${M_SHORT}" + echo -e "${CTX_BAR}${DOT}${BG_FMT}${DOT}${QUOTA_FMT}" fi