#!/usr/bin/env zsh # Core utility functions for forge plugin # Lazy loader for commands cache # Loads the commands list only when first needed, avoiding startup cost function _forge_get_commands() { if [[ -z "$_FORGE_COMMANDS" ]]; then _FORGE_COMMANDS="$(CLICOLOR_FORCE=0 $_FORGE_BIN list commands --porcelain 2>/dev/null)" fi echo "$_FORGE_COMMANDS" } # Helper function to execute forge commands consistently # This ensures proper handling of special characters and consistent output function _forge_exec() { local agent_id="${_FORGE_ACTIVE_AGENT:-forge}" local -a cmd cmd=($_FORGE_BIN --agent "$agent_id") # Expose terminal context arrays as US-separated (\x1F) env vars so that # the Rust TerminalContextService can read them via get_env_var. # ASCII Unit Separator (\x1F) is used instead of `:` because commands # can legitimately contain colons (URLs, port mappings, paths, etc.). # Use `local -x` so the variables are exported only to the child forge # process and do not leak into the caller's shell environment. if [[ "$_FORGE_TERM" == "true" && ${#_FORGE_TERM_COMMANDS} -gt 0 ]]; then # Join the ring-buffer arrays with the ASCII Unit Separator (\x1F). # We use IFS-based joining ("${arr[*]}") rather than ${(j.SEP.)arr} because # zsh does NOT expand $'...' ANSI-C escapes inside parameter expansion flags. local _old_ifs="$IFS" _sep=$'\x1f' IFS="$_sep" local -x _FORGE_TERM_COMMANDS="${_FORGE_TERM_COMMANDS[*]}" local -x _FORGE_TERM_EXIT_CODES="${_FORGE_TERM_EXIT_CODES[*]}" local -x _FORGE_TERM_TIMESTAMPS="${_FORGE_TERM_TIMESTAMPS[*]}" IFS="$_old_ifs" fi cmd+=("$@") [[ -n "$_FORGE_SESSION_MODEL" ]] && local -x FORGE_SESSION__MODEL_ID="$_FORGE_SESSION_MODEL" [[ -n "$_FORGE_SESSION_PROVIDER" ]] && local -x FORGE_SESSION__PROVIDER_ID="$_FORGE_SESSION_PROVIDER" [[ -n "$_FORGE_SESSION_REASONING_EFFORT" ]] && local -x FORGE_REASONING__EFFORT="$_FORGE_SESSION_REASONING_EFFORT" "${cmd[@]}" } # Like _forge_exec but connects stdin/stdout to /dev/tty so that interactive # prompts (rustyline, nucleo-picker, etc.) work correctly when forge is launched as a # child of a ZLE widget. ZLE owns the terminal and replaces the process's # stdin/stdout with its own pipes, so without this redirect any readline # library would see a non-tty stdin and return EOF immediately. # Do NOT use inside $(...) command substitutions - use _forge_exec instead. function _forge_exec_interactive() { local agent_id="${_FORGE_ACTIVE_AGENT:-forge}" local -a cmd cmd=($_FORGE_BIN --agent "$agent_id") # Expose terminal context arrays as US-separated (\x1F) env vars so that # the Rust TerminalContextService can read them via get_env_var. # ASCII Unit Separator (\x1F) is used instead of `:` because commands # can legitimately contain colons (URLs, port mappings, paths, etc.). # Use `local -x` so the variables are exported only for the duration of # this function call (i.e. inherited by the child forge process) and do # not leak into the caller's shell environment. if [[ "$_FORGE_TERM" == "true" && ${#_FORGE_TERM_COMMANDS} -gt 0 ]]; then local _old_ifs="$IFS" _sep=$'\x1f' IFS="$_sep" local -x _FORGE_TERM_COMMANDS="${_FORGE_TERM_COMMANDS[*]}" local -x _FORGE_TERM_EXIT_CODES="${_FORGE_TERM_EXIT_CODES[*]}" local -x _FORGE_TERM_TIMESTAMPS="${_FORGE_TERM_TIMESTAMPS[*]}" IFS="$_old_ifs" fi cmd+=("$@") [[ -n "$_FORGE_SESSION_MODEL" ]] && local -x FORGE_SESSION__MODEL_ID="$_FORGE_SESSION_MODEL" [[ -n "$_FORGE_SESSION_PROVIDER" ]] && local -x FORGE_SESSION__PROVIDER_ID="$_FORGE_SESSION_PROVIDER" [[ -n "$_FORGE_SESSION_REASONING_EFFORT" ]] && local -x FORGE_REASONING__EFFORT="$_FORGE_SESSION_REASONING_EFFORT" "${cmd[@]}" /dev/tty } function _forge_select() { [[ -n "$_FORGE_SESSION_MODEL" ]] && local -x FORGE_SESSION__MODEL_ID="$_FORGE_SESSION_MODEL" [[ -n "$_FORGE_SESSION_PROVIDER" ]] && local -x FORGE_SESSION__PROVIDER_ID="$_FORGE_SESSION_PROVIDER" [[ -n "$_FORGE_SESSION_REASONING_EFFORT" ]] && local -x FORGE_REASONING__EFFORT="$_FORGE_SESSION_REASONING_EFFORT" CLICOLOR_FORCE=0 $_FORGE_BIN select "$@" /dev/tty } function _forge_select_global() { CLICOLOR_FORCE=0 $_FORGE_BIN select "$@" /dev/tty } function _forge_select_with_query() { local query="$1" shift if [[ -n "$query" ]]; then _forge_select "$@" --query "$query" else _forge_select "$@" fi } function _forge_select_with_query_global() { local query="$1" shift if [[ -n "$query" ]]; then _forge_select_global "$@" --query "$query" else _forge_select_global "$@" fi } function _forge_select_model_pair() { local result result=$(_forge_select_with_query "$1" model) if [[ -z "$result" ]]; then reply=() return 1 fi reply=("${(@f)result}") [[ ${#reply[@]} -ge 2 ]] } function _forge_select_model_pair_global() { local result result=$(_forge_select_with_query_global "$1" model) if [[ -z "$result" ]]; then reply=() return 1 fi reply=("${(@f)result}") [[ ${#reply[@]} -ge 2 ]] } function _forge_reset() { # Print newlines equal to the current buffer display line count so that # ZLE's zrefresh() clears these padding lines instead of conversation # output. When BUFFER spans multiple display lines (newlines or terminal # wrap), ZLE tracks the multi-line state internally (olnct). On # reset-prompt, zrefresh() compares olnct (old multi-line count) against # nlnct (new count after clearing) and clears the delta lines. By # printing padding here, the cleared lines are blank ones we inserted # rather than the forge conversation output that precedes them. local pad="${BUFFERLINES:-1}" _i for ((_i=1; _i # Levels: error, info, success, warning, debug # Color scheme matches crates/forge_main/src/title_display.rs function _forge_log() { local level="$1" local message="$2" local timestamp="\033[90m[$(date '+%H:%M:%S')]\033[0m" case "$level" in error) # Category::Error - Red ⏺ echo "\033[31m⏺\033[0m ${timestamp} \033[31m${message}\033[0m" ;; info) # Category::Info - White ⏺ echo "\033[37m⏺\033[0m ${timestamp} \033[37m${message}\033[0m" ;; success) # Category::Action/Completion - Yellow ⏺ echo "\033[33m⏺\033[0m ${timestamp} \033[37m${message}\033[0m" ;; warning) # Category::Warning - Bright yellow ⚠️ echo "\033[93m⚠️\033[0m ${timestamp} \033[93m${message}\033[0m" ;; debug) # Category::Debug - Cyan ⏺ with dimmed text echo "\033[36m⏺\033[0m ${timestamp} \033[90m${message}\033[0m" ;; *) echo "${message}" ;; esac } # Helper function to check if a workspace is indexed # Usage: _forge_is_workspace_indexed # Returns: 0 if workspace is indexed, 1 otherwise function _forge_is_workspace_indexed() { local workspace_path="$1" $_FORGE_BIN workspace info "$workspace_path" >/dev/null 2>&1 return $? } # Start background sync job for current workspace if not already running # Uses canonical path hash to identify workspace function _forge_start_background_sync() { # Check if sync is enabled (default to true if not set) local sync_enabled="${FORGE_SYNC_ENABLED:-true}" if [[ "$sync_enabled" != "true" ]]; then return 0 fi # Get canonical workspace path local workspace_path=$(pwd -P) # Check if workspace is indexed before attempting sync { # Run sync once in background # Close all output streams immediately to prevent any flashing # Redirect stdin to /dev/null to prevent hanging if sync tries to read input exec >/dev/null 2>&1 /dev/null 2>&1