From cd66c5695aedee6b1b556c57f64f1b6eeba188ff Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Thu, 5 Feb 2026 14:39:12 +0800 Subject: [PATCH] refactor(ai): replace empty buffer with min input - Replace ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER with AI_MIN_INPUT - Add ZSH_AUTOSUGGEST_AI_DEBUG environment variable - Add debug logging function to diagnose failures - Update history lines default from 20 to 5 - Update pwd history preference default to no Min input provides clearer semantics: set to 0 for empty-buffer suggestions or higher to require minimum input. Debug logging helps diagnose missing suggestions by showing API request flow. Co-Authored-By: Claude Haiku 4.5 --- README.md | 22 ++++++++--- spec/strategies/ai_spec.rb | 12 +++--- src/config.zsh | 12 +++--- src/start.zsh | 3 +- src/strategies/ai.zsh | 56 +++++++++++++++++++++----- src/widgets.zsh | 9 +++-- zsh-autosuggestions.zsh | 80 ++++++++++++++++++++++++++++---------- 7 files changed, 142 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index dfa1290..067d8ef 100644 --- a/README.md +++ b/README.md @@ -130,17 +130,17 @@ export ZSH_AUTOSUGGEST_STRATEGY=(ai history) | `ZSH_AUTOSUGGEST_AI_ENDPOINT` | `https://api.openai.com/v1` | API base URL | | `ZSH_AUTOSUGGEST_AI_MODEL` | `gpt-3.5-turbo` | Model name to use | | `ZSH_AUTOSUGGEST_AI_TIMEOUT` | `5` | Request timeout in seconds | -| `ZSH_AUTOSUGGEST_AI_MIN_INPUT` | `0` | Minimum input length before querying | -| `ZSH_AUTOSUGGEST_AI_HISTORY_LINES` | `20` | Number of recent history lines to send as context | -| `ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY` | `yes` | Prioritize history from current directory | -| `ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER` | (unset) | Set to any value to enable suggestions on empty buffer (requires zsh 5.3+) | +| `ZSH_AUTOSUGGEST_AI_MIN_INPUT` | `1` | Minimum input length before querying (`0` enables empty-buffer suggestions) | +| `ZSH_AUTOSUGGEST_AI_HISTORY_LINES` | `5` | Number of recent history lines to send as context | +| `ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY` | `no` | Prioritize history from current directory | +| `ZSH_AUTOSUGGEST_AI_DEBUG` | (unset) | Prints AI debug logs to stderr when enabled | #### Empty Buffer Suggestions -By default, suggestions only appear when you start typing. You can enable suggestions on an empty command line by setting `ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER`: +By default, suggestions only appear when you start typing. You can enable suggestions on an empty command line by setting `ZSH_AUTOSUGGEST_AI_MIN_INPUT=0`: ```sh -export ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER=1 +export ZSH_AUTOSUGGEST_AI_MIN_INPUT=0 export ZSH_AUTOSUGGEST_AI_API_KEY="your-api-key-here" export ZSH_AUTOSUGGEST_STRATEGY=(ai history) ``` @@ -151,6 +151,16 @@ export ZSH_AUTOSUGGEST_STRATEGY=(ai history) - Traditional strategies (history, completion) don't benefit from empty buffer suggestions - **Cost consideration:** With AI strategy, this will make an API request on every new prompt, which may increase API costs +#### AI Debug Logs + +If AI suggestions are not appearing, enable debug logs: + +```sh +export ZSH_AUTOSUGGEST_AI_DEBUG=1 +``` + +Debug logs are printed to stderr with the prefix `[zsh-autosuggestions ai]`. + #### Examples **OpenAI (default):** diff --git a/spec/strategies/ai_spec.rb b/spec/strategies/ai_spec.rb index cc1ff24..072e477 100644 --- a/spec/strategies/ai_spec.rb +++ b/spec/strategies/ai_spec.rb @@ -195,7 +195,7 @@ EOFCURL end context 'empty buffer suggestions' do - let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER=1", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] } + let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_AI_MIN_INPUT=0", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] } let(:before_sourcing) do -> { @@ -216,8 +216,8 @@ EOFCURL end end - context 'empty buffer without flag' do - let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai history)"] } + context 'empty buffer with default min input' do + let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_AI_MIN_INPUT=1", "ZSH_AUTOSUGGEST_STRATEGY=(ai history)"] } let(:before_sourcing) do -> { @@ -232,7 +232,7 @@ EOFCURL } end - it 'does not suggest on empty buffer by default' do + it 'does not suggest on empty buffer when min input is 1' do with_history('git status') do sleep 0.5 expect(session.content).to_not match(/git status/) @@ -323,7 +323,7 @@ EOFCURL end context 'dual prompt modes' do - let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER=1", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] } + let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_AI_MIN_INPUT=0", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] } context 'empty buffer mode' do let(:before_sourcing) do @@ -366,7 +366,7 @@ EOFCURL end context 'temperature configuration' do - let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER=1", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] } + let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_AI_MIN_INPUT=0", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] } let(:before_sourcing) do -> { diff --git a/src/config.zsh b/src/config.zsh index 0fb9d37..7ed62c4 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -108,17 +108,17 @@ typeset -g ZSH_AUTOSUGGEST_AI_MODEL='gpt-3.5-turbo' typeset -g ZSH_AUTOSUGGEST_AI_TIMEOUT=5 # Minimum input length before querying AI +# Set to 0 to allow empty-buffer AI suggestions (( ! ${+ZSH_AUTOSUGGEST_AI_MIN_INPUT} )) && -typeset -g ZSH_AUTOSUGGEST_AI_MIN_INPUT=0 +typeset -g ZSH_AUTOSUGGEST_AI_MIN_INPUT=1 # Number of recent history lines to include as context (( ! ${+ZSH_AUTOSUGGEST_AI_HISTORY_LINES} )) && -typeset -g ZSH_AUTOSUGGEST_AI_HISTORY_LINES=20 +typeset -g ZSH_AUTOSUGGEST_AI_HISTORY_LINES=5 # Prefer history entries from current directory (( ! ${+ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY} )) && -typeset -g ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY=yes +typeset -g ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY=no -# Allow suggestions on empty buffer (opt-in, for AI strategy) -# Set to any value to enable. Unset by default. -# Uses (( ${+VAR} )) pattern like ZSH_AUTOSUGGEST_MANUAL_REBIND +# Enable AI debug logs to stderr (opt-in). +# Set to any value except 0/false/no/off to enable. diff --git a/src/start.zsh b/src/start.zsh index 2cdf96b..4c39008 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -34,7 +34,8 @@ add-zsh-hook precmd _zsh_autosuggest_start _zsh_autosuggest_line_init() { emulate -L zsh - if (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )) && \ + local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}" + if (( min_input == 0 )) && \ (( ! ${+_ZSH_AUTOSUGGEST_DISABLED} )); then _zsh_autosuggest_fetch fi diff --git a/src/strategies/ai.zsh b/src/strategies/ai.zsh index afcdfbe..097156e 100644 --- a/src/strategies/ai.zsh +++ b/src/strategies/ai.zsh @@ -35,8 +35,8 @@ _zsh_autosuggest_strategy_ai_gather_context() { # Reset options to defaults and enable LOCAL_OPTIONS emulate -L zsh - local max_lines="${ZSH_AUTOSUGGEST_AI_HISTORY_LINES:-20}" - local prefer_pwd="${ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY:-yes}" + local max_lines="${ZSH_AUTOSUGGEST_AI_HISTORY_LINES:-5}" + local prefer_pwd="${ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY:-no}" local pwd_basename="${PWD:t}" local -a context_lines local -a pwd_lines @@ -141,6 +141,18 @@ _zsh_autosuggest_strategy_ai_normalize() { printf '%s' "$result" } +_zsh_autosuggest_strategy_ai_debug_log() { + # Reset options to defaults and enable LOCAL_OPTIONS + emulate -L zsh + + local debug="${ZSH_AUTOSUGGEST_AI_DEBUG:-0}" + case "${debug:l}" in + 0|false|no|off) return ;; + esac + + print -ru2 -- "[zsh-autosuggestions ai] $1" +} + _zsh_autosuggest_strategy_ai() { # Reset options to defaults and enable LOCAL_OPTIONS emulate -L zsh @@ -149,14 +161,23 @@ _zsh_autosuggest_strategy_ai() { local buffer="$1" # Early return if API key not set (opt-in gate) - [[ -z "$ZSH_AUTOSUGGEST_AI_API_KEY" ]] && return + if [[ -z "$ZSH_AUTOSUGGEST_AI_API_KEY" ]]; then + _zsh_autosuggest_strategy_ai_debug_log "API key not set; skipping AI request." + return + fi # Early return if curl or jq not available - [[ -z "${commands[curl]}" ]] || [[ -z "${commands[jq]}" ]] && return + if [[ -z "${commands[curl]}" ]] || [[ -z "${commands[jq]}" ]]; then + _zsh_autosuggest_strategy_ai_debug_log "Missing dependency: curl and jq are required." + return + fi # Early return if input too short - local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-0}" - [[ ${#buffer} -lt $min_input ]] && return + local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}" + if [[ ${#buffer} -lt $min_input ]]; then + _zsh_autosuggest_strategy_ai_debug_log "Input shorter than ZSH_AUTOSUGGEST_AI_MIN_INPUT=$min_input." + return + fi # Gather history context local -a context @@ -222,6 +243,8 @@ _zsh_autosuggest_strategy_ai() { local timeout="${ZSH_AUTOSUGGEST_AI_TIMEOUT:-5}" local response + _zsh_autosuggest_strategy_ai_debug_log "Requesting $endpoint (model=${ZSH_AUTOSUGGEST_AI_MODEL:-gpt-3.5-turbo}, input_len=${#buffer})." + response=$(curl --silent --max-time "$timeout" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ZSH_AUTOSUGGEST_AI_API_KEY" \ @@ -230,26 +253,39 @@ _zsh_autosuggest_strategy_ai() { "$endpoint" 2>/dev/null) # Check curl exit status - [[ $? -ne 0 ]] && return + local curl_status=$? + if [[ $curl_status -ne 0 ]]; then + _zsh_autosuggest_strategy_ai_debug_log "curl failed with exit code $curl_status." + return + fi # Split response body from HTTP status local http_code="${response##*$'\n'}" local body="${response%$'\n'*}" # Early return on non-2xx status - [[ "$http_code" != 2* ]] && return + if [[ "$http_code" != 2* ]]; then + _zsh_autosuggest_strategy_ai_debug_log "HTTP $http_code from AI endpoint." + return + fi # Extract content from JSON response local content content=$(printf '%s' "$body" | jq -r '.choices[0].message.content // empty' 2>/dev/null) # Early return if extraction failed - [[ -z "$content" ]] && return + if [[ -z "$content" ]]; then + _zsh_autosuggest_strategy_ai_debug_log "No suggestion content in API response." + return + fi # Normalize response local normalized normalized="$(_zsh_autosuggest_strategy_ai_normalize "$content" "$buffer")" # Set suggestion - [[ -n "$normalized" ]] && suggestion="$normalized" + if [[ -n "$normalized" ]]; then + suggestion="$normalized" + _zsh_autosuggest_strategy_ai_debug_log "AI suggestion accepted: '$normalized'." + fi } diff --git a/src/widgets.zsh b/src/widgets.zsh index 78b2bd8..935a5a8 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -12,8 +12,9 @@ _zsh_autosuggest_disable() { # Enable suggestions _zsh_autosuggest_enable() { unset _ZSH_AUTOSUGGEST_DISABLED + local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}" - if (( $#BUFFER )) || (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )); then + if (( $#BUFFER )) || (( min_input == 0 )); then _zsh_autosuggest_fetch fi } @@ -73,11 +74,12 @@ _zsh_autosuggest_modify() { fi # Get a new suggestion if the buffer is not empty after modification + local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}" if (( $#BUFFER > 0 )); then if [[ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]] || (( $#BUFFER <= $ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE )); then _zsh_autosuggest_fetch fi - elif (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )); then + elif (( min_input == 0 )); then _zsh_autosuggest_fetch fi @@ -100,8 +102,9 @@ _zsh_autosuggest_suggest() { emulate -L zsh local suggestion="$1" + local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}" - if [[ -n "$suggestion" ]] && { (( $#BUFFER )) || (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )); }; then + if [[ -n "$suggestion" ]] && { (( $#BUFFER )) || (( min_input == 0 )); }; then POSTDISPLAY="${suggestion#$BUFFER}" else POSTDISPLAY= diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 56b0cc5..66f13b2 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -134,20 +134,20 @@ typeset -g ZSH_AUTOSUGGEST_AI_MODEL='gpt-3.5-turbo' typeset -g ZSH_AUTOSUGGEST_AI_TIMEOUT=5 # Minimum input length before querying AI +# Set to 0 to allow empty-buffer AI suggestions (( ! ${+ZSH_AUTOSUGGEST_AI_MIN_INPUT} )) && -typeset -g ZSH_AUTOSUGGEST_AI_MIN_INPUT=0 +typeset -g ZSH_AUTOSUGGEST_AI_MIN_INPUT=1 # Number of recent history lines to include as context (( ! ${+ZSH_AUTOSUGGEST_AI_HISTORY_LINES} )) && -typeset -g ZSH_AUTOSUGGEST_AI_HISTORY_LINES=20 +typeset -g ZSH_AUTOSUGGEST_AI_HISTORY_LINES=5 # Prefer history entries from current directory (( ! ${+ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY} )) && -typeset -g ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY=yes +typeset -g ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY=no -# Allow suggestions on empty buffer (opt-in, for AI strategy) -# Set to any value to enable. Unset by default. -# Uses (( ${+VAR} )) pattern like ZSH_AUTOSUGGEST_MANUAL_REBIND +# Enable AI debug logs to stderr (opt-in). +# Set to any value except 0/false/no/off to enable. #--------------------------------------------------------------------# # Utility Functions # @@ -305,8 +305,9 @@ _zsh_autosuggest_disable() { # Enable suggestions _zsh_autosuggest_enable() { unset _ZSH_AUTOSUGGEST_DISABLED + local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}" - if (( $#BUFFER )) || (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )); then + if (( $#BUFFER )) || (( min_input == 0 )); then _zsh_autosuggest_fetch fi } @@ -366,11 +367,12 @@ _zsh_autosuggest_modify() { fi # Get a new suggestion if the buffer is not empty after modification + local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}" if (( $#BUFFER > 0 )); then if [[ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]] || (( $#BUFFER <= $ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE )); then _zsh_autosuggest_fetch fi - elif (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )); then + elif (( min_input == 0 )); then _zsh_autosuggest_fetch fi @@ -393,8 +395,9 @@ _zsh_autosuggest_suggest() { emulate -L zsh local suggestion="$1" + local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}" - if [[ -n "$suggestion" ]] && { (( $#BUFFER )) || (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )); }; then + if [[ -n "$suggestion" ]] && { (( $#BUFFER )) || (( min_input == 0 )); }; then POSTDISPLAY="${suggestion#$BUFFER}" else POSTDISPLAY= @@ -561,8 +564,8 @@ _zsh_autosuggest_strategy_ai_gather_context() { # Reset options to defaults and enable LOCAL_OPTIONS emulate -L zsh - local max_lines="${ZSH_AUTOSUGGEST_AI_HISTORY_LINES:-20}" - local prefer_pwd="${ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY:-yes}" + local max_lines="${ZSH_AUTOSUGGEST_AI_HISTORY_LINES:-5}" + local prefer_pwd="${ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY:-no}" local pwd_basename="${PWD:t}" local -a context_lines local -a pwd_lines @@ -667,6 +670,18 @@ _zsh_autosuggest_strategy_ai_normalize() { printf '%s' "$result" } +_zsh_autosuggest_strategy_ai_debug_log() { + # Reset options to defaults and enable LOCAL_OPTIONS + emulate -L zsh + + local debug="${ZSH_AUTOSUGGEST_AI_DEBUG:-0}" + case "${debug:l}" in + 0|false|no|off) return ;; + esac + + print -ru2 -- "[zsh-autosuggestions ai] $1" +} + _zsh_autosuggest_strategy_ai() { # Reset options to defaults and enable LOCAL_OPTIONS emulate -L zsh @@ -675,14 +690,23 @@ _zsh_autosuggest_strategy_ai() { local buffer="$1" # Early return if API key not set (opt-in gate) - [[ -z "$ZSH_AUTOSUGGEST_AI_API_KEY" ]] && return + if [[ -z "$ZSH_AUTOSUGGEST_AI_API_KEY" ]]; then + _zsh_autosuggest_strategy_ai_debug_log "API key not set; skipping AI request." + return + fi # Early return if curl or jq not available - [[ -z "${commands[curl]}" ]] || [[ -z "${commands[jq]}" ]] && return + if [[ -z "${commands[curl]}" ]] || [[ -z "${commands[jq]}" ]]; then + _zsh_autosuggest_strategy_ai_debug_log "Missing dependency: curl and jq are required." + return + fi # Early return if input too short - local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-0}" - [[ ${#buffer} -lt $min_input ]] && return + local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}" + if [[ ${#buffer} -lt $min_input ]]; then + _zsh_autosuggest_strategy_ai_debug_log "Input shorter than ZSH_AUTOSUGGEST_AI_MIN_INPUT=$min_input." + return + fi # Gather history context local -a context @@ -748,6 +772,8 @@ _zsh_autosuggest_strategy_ai() { local timeout="${ZSH_AUTOSUGGEST_AI_TIMEOUT:-5}" local response + _zsh_autosuggest_strategy_ai_debug_log "Requesting $endpoint (model=${ZSH_AUTOSUGGEST_AI_MODEL:-gpt-3.5-turbo}, input_len=${#buffer})." + response=$(curl --silent --max-time "$timeout" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ZSH_AUTOSUGGEST_AI_API_KEY" \ @@ -756,28 +782,41 @@ _zsh_autosuggest_strategy_ai() { "$endpoint" 2>/dev/null) # Check curl exit status - [[ $? -ne 0 ]] && return + local curl_status=$? + if [[ $curl_status -ne 0 ]]; then + _zsh_autosuggest_strategy_ai_debug_log "curl failed with exit code $curl_status." + return + fi # Split response body from HTTP status local http_code="${response##*$'\n'}" local body="${response%$'\n'*}" # Early return on non-2xx status - [[ "$http_code" != 2* ]] && return + if [[ "$http_code" != 2* ]]; then + _zsh_autosuggest_strategy_ai_debug_log "HTTP $http_code from AI endpoint." + return + fi # Extract content from JSON response local content content=$(printf '%s' "$body" | jq -r '.choices[0].message.content // empty' 2>/dev/null) # Early return if extraction failed - [[ -z "$content" ]] && return + if [[ -z "$content" ]]; then + _zsh_autosuggest_strategy_ai_debug_log "No suggestion content in API response." + return + fi # Normalize response local normalized normalized="$(_zsh_autosuggest_strategy_ai_normalize "$content" "$buffer")" # Set suggestion - [[ -n "$normalized" ]] && suggestion="$normalized" + if [[ -n "$normalized" ]]; then + suggestion="$normalized" + _zsh_autosuggest_strategy_ai_debug_log "AI suggestion accepted: '$normalized'." + fi } #--------------------------------------------------------------------# @@ -1154,7 +1193,8 @@ add-zsh-hook precmd _zsh_autosuggest_start _zsh_autosuggest_line_init() { emulate -L zsh - if (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )) && \ + local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}" + if (( min_input == 0 )) && \ (( ! ${+_ZSH_AUTOSUGGEST_DISABLED} )); then _zsh_autosuggest_fetch fi