Created
September 13, 2026 23:51
-
-
Save zonca/900fa78ad943a971485d1affad82f291 to your computer and use it in GitHub Desktop.
agent-state: set hterm terminal background to reflect AI agent status (ChromeOS Overview)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # agent-state: set the hterm terminal background based on an AI agent's status, | |
| # so ChromeOS Overview shows at a glance which terminals are working vs done. | |
| # | |
| # Usage: | |
| # agent-state <working|done|waiting|error|reset> [agent-name] | |
| # | |
| # States: | |
| # working -> yellow background (traffic light: agent is working) | |
| # done -> green background (agent finished) | |
| # error -> red background (agent hit an error) | |
| # waiting -> yellow background (awaiting input) | |
| # reset -> reset to default background and forget state | |
| # | |
| # It finds the real tty by walking up the process tree (hooks usually run with | |
| # stdin/stdout not connected to the terminal) and writes OSC escape sequences: | |
| # OSC 11 (set background color), OSC 111 (reset background color) | |
| set -u | |
| STATE="${1:-}" | |
| STATE_DIR="${AGENT_STATE_DIR:-$HOME/.local/state/agent-state}" | |
| WORKING_COLOR="${AGENT_STATE_WORKING_COLOR:-#ffd700}" | |
| DONE_COLOR="${AGENT_STATE_DONE_COLOR:-#22c55e}" | |
| WAITING_COLOR="${AGENT_STATE_WAITING_COLOR:-#d4a017}" | |
| ERROR_COLOR="${AGENT_STATE_ERROR_COLOR:-#dc2626}" | |
| find_tty() { | |
| local pid=$$ t | |
| for fd in 0 1 2; do | |
| t=$(readlink "/proc/$pid/fd/$fd" 2>/dev/null) | |
| case "$t" in | |
| /dev/tty|/dev/pts/[0-9]*|/dev/tty[0-9]*) printf '%s' "$t"; return 0 ;; | |
| esac | |
| done | |
| while [ "$pid" -gt 1 ]; do | |
| for fd in 0 1 2; do | |
| t=$(readlink "/proc/$pid/fd/$fd" 2>/dev/null) | |
| case "$t" in | |
| /dev/tty|/dev/pts/[0-9]*|/dev/tty[0-9]*) printf '%s' "$t"; return 0 ;; | |
| esac | |
| done | |
| stat=$(cat "/proc/$pid/stat" 2>/dev/null) || return 1 | |
| stat=${stat##*) } | |
| pid=$(awk '{print $2}' <<< "$stat") | |
| [ -z "$pid" ] || [ "$pid" = "0" ] && return 1 | |
| done | |
| return 1 | |
| } | |
| usage() { | |
| echo "usage: agent-state <working|done|waiting|error|reset> [agent-name]" >&2 | |
| exit 2 | |
| } | |
| case "$STATE" in | |
| working|done|waiting|error|reset) ;; | |
| "") usage ;; | |
| *) usage ;; | |
| esac | |
| # Allow explicit tty override for testing | |
| TTY="${AGENT_STATE_TTY:-}" | |
| if [ -z "$TTY" ]; then | |
| TTY=$(find_tty) || exit 0 | |
| fi | |
| [ -e "$TTY" ] || exit 0 | |
| key=$(printf '%s' "$TTY" | tr '/' '_') | |
| last_state="" | |
| if [ -f "$STATE_DIR/$key" ]; then | |
| last_state=$(cat "$STATE_DIR/$key" 2>/dev/null) | |
| fi | |
| # only mark done if we previously saw the agent busy/working, unless forced | |
| # (avoid painting freshly opened terminals before any work happened) | |
| if [ "$STATE" = "done" ] && [ -z "$last_state" ] && [ -z "${AGENT_STATE_FORCE:-}" ]; then | |
| exit 0 | |
| fi | |
| case "$STATE" in | |
| working) seq='\033]11;'"$WORKING_COLOR"'\007' ;; | |
| done) seq='\033]11;'"$DONE_COLOR"'\007' ;; | |
| waiting) seq='\033]11;'"$WAITING_COLOR"'\007' ;; | |
| error) seq='\033]11;'"$ERROR_COLOR"'\007' ;; | |
| reset) seq='\033]111;\007' ;; | |
| esac | |
| printf '%b' "$seq" > "$TTY" 2>/dev/null || exit 0 | |
| mkdir -p "$STATE_DIR" 2>/dev/null | |
| if [ "$STATE" = "reset" ]; then | |
| rm -f "$STATE_DIR/$key" | |
| else | |
| printf '%s' "$STATE" > "$STATE_DIR/$key" | |
| fi | |
| exit 0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { appendFileSync } from "node:fs" | |
| const LOG = "/tmp/opencode-agent-events.log" | |
| export const AgentStatus = async ({ $ }) => { | |
| let q = Promise.resolve() | |
| const run = (state) => { | |
| q = q.then(async () => { | |
| try { | |
| await $`/home/zonca/.local/bin/agent-state ${state} opencode` | |
| } catch {} | |
| }) | |
| return q | |
| } | |
| return { | |
| event: async ({ event }) => { | |
| const sid = event.properties?.sessionID ?? "?" | |
| const st = event.properties?.status?.type ?? "" | |
| try { | |
| appendFileSync(LOG, `${new Date().toISOString()} ${event.type} ${st} ${sid}\n`) | |
| } catch {} | |
| if (event.type === "session.status") { | |
| if (st === "busy" || st === "retry") { | |
| await run("working") | |
| } else if (st === "idle") { | |
| await run("done") | |
| } | |
| } else if (event.type === "session.idle") { | |
| await run("done") | |
| } else if (event.type === "session.error") { | |
| await run("error") | |
| } | |
| }, | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment