feat(ai): add empty buffer context suggestions

Enable AI suggestions on empty prompts with enhanced
environmental context.

- Update AI_MIN_INPUT default from 3 to 0
- Add ALLOW_EMPTY_BUFFER opt-in config variable
- Remove empty-buffer guards in modify, suggest, enable
- Add zle-line-init hook for prompt-time suggestions
- Enhance history gathering with PWD-aware priority
- Add env context for dir listing, git branch, status
- Implement dual prompts: predict vs complete modes
- Add prompt artifact stripping for $ and > prefixes
- Update README with empty buffer configuration
- Add tests for empty buffer and artifact stripping

Empty buffer suggestions require zsh 5.3+ and work best with
AI strategy, leveraging directory context, git state, and
command history to predict likely next actions.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Frad LEE 2026-02-05 10:58:19 +08:00
commit d89bf4ec0d
7 changed files with 282 additions and 31 deletions

View file

@ -109,7 +109,7 @@ typeset -g ZSH_AUTOSUGGEST_AI_TIMEOUT=5
# Minimum input length before querying AI
(( ! ${+ZSH_AUTOSUGGEST_AI_MIN_INPUT} )) &&
typeset -g ZSH_AUTOSUGGEST_AI_MIN_INPUT=3
typeset -g ZSH_AUTOSUGGEST_AI_MIN_INPUT=0
# Number of recent history lines to include as context
(( ! ${+ZSH_AUTOSUGGEST_AI_HISTORY_LINES} )) &&
@ -118,3 +118,7 @@ typeset -g ZSH_AUTOSUGGEST_AI_HISTORY_LINES=20
# Prefer history entries from current directory
(( ! ${+ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY} )) &&
typeset -g ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY=yes
# 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

View file

@ -31,3 +31,17 @@ fi
# Start the autosuggestion widgets on the next precmd
add-zsh-hook precmd _zsh_autosuggest_start
_zsh_autosuggest_line_init() {
emulate -L zsh
if (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )) && \
(( ! ${+_ZSH_AUTOSUGGEST_DISABLED} )); then
_zsh_autosuggest_fetch
fi
}
# Use add-zle-hook-widget (zsh 5.3+) to avoid conflicts with other plugins
if (( ${+functions[add-zle-hook-widget]} )) || \
autoload -Uz add-zle-hook-widget 2>/dev/null; then
add-zle-hook-widget zle-line-init _zsh_autosuggest_line_init
fi

View file

@ -1,6 +1,6 @@
#--------------------------------------------------------------------#
# AI Suggestion Strategy #
# AI Suggestion Strategy #
#--------------------------------------------------------------------#
# Queries an OpenAI-compatible LLM API to generate command
# completions based on partial input, working directory, and
@ -50,22 +50,54 @@ _zsh_autosuggest_strategy_ai_gather_context() {
line="${line:0:200}..."
fi
# Categorize by PWD relevance
if [[ "$prefer_pwd" == "yes" ]] && [[ "$line" == *"$pwd_basename"* || "$line" == *"./"* || "$line" == *"../"* ]]; then
# Categorize by PWD relevance - match full path, basename, or PWD-relevant commands
if [[ "$prefer_pwd" == "yes" ]] && [[ "$line" == *"$PWD"* || "$line" == *"$pwd_basename"* || "$line" == cd* || "$line" == ls* || "$line" == "git "* || "$line" == *"./"* || "$line" == *"../"* ]]; then
pwd_lines+=("$line")
else
other_lines+=("$line")
fi
done
# Cap PWD lines at 2/3 of max to maintain diversity
local pwd_max=$(( (max_lines * 2) / 3 ))
local pwd_count=${#pwd_lines}
[[ $pwd_count -gt $pwd_max ]] && pwd_count=$pwd_max
# Prioritize PWD-relevant lines, then fill with others
context_lines=("${pwd_lines[@]}" "${other_lines[@]}")
context_lines=("${(@)pwd_lines[1,$pwd_count]}" "${other_lines[@]}")
context_lines=("${(@)context_lines[1,$max_lines]}")
# Return via reply array
reply=("${context_lines[@]}")
}
_zsh_autosuggest_strategy_ai_gather_env_context() {
# Reset options to defaults and enable LOCAL_OPTIONS
emulate -L zsh
local -A env_info
# Directory listing (up to 20 entries)
local dir_contents
dir_contents=$(command ls -1 2>/dev/null | head -20 | tr '\n' ', ' | sed 's/, $//')
[[ -n "$dir_contents" ]] && env_info[dir_contents]="$dir_contents"
# Git branch (try two methods)
local git_branch
git_branch=$(command git branch --show-current 2>/dev/null)
[[ -z "$git_branch" ]] && git_branch=$(command git rev-parse --abbrev-ref HEAD 2>/dev/null)
[[ -n "$git_branch" ]] && env_info[git_branch]="$git_branch"
# Git status (up to 10 lines)
local git_status
git_status=$(command git status --porcelain 2>/dev/null | head -10 | tr '\n' '; ' | sed 's/; $//')
[[ -n "$git_status" ]] && env_info[git_status]="$git_status"
# Return via reply associative array
typeset -gA reply
reply=("${(@kv)env_info}")
}
_zsh_autosuggest_strategy_ai_normalize() {
# Reset options to defaults and enable LOCAL_OPTIONS
emulate -L zsh
@ -77,6 +109,10 @@ _zsh_autosuggest_strategy_ai_normalize() {
# Strip \r
response="${response//$'\r'/}"
# Strip leading prompt artifacts ($ or >)
response="${response##\$ }"
response="${response##> }"
# Strip markdown code fences
response="${response##\`\`\`*$'\n'}"
response="${response%%$'\n'\`\`\`}"
@ -119,14 +155,19 @@ _zsh_autosuggest_strategy_ai() {
[[ -z "${commands[curl]}" ]] || [[ -z "${commands[jq]}" ]] && return
# Early return if input too short
local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-3}"
local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-0}"
[[ ${#buffer} -lt $min_input ]] && return
# Gather context
# Gather history context
local -a context
_zsh_autosuggest_strategy_ai_gather_context
context=("${reply[@]}")
# Gather environment context
local -A env_context
_zsh_autosuggest_strategy_ai_gather_env_context
env_context=("${(@kv)reply}")
# Build context string
local context_str=""
for line in "${context[@]}"; do
@ -136,23 +177,44 @@ _zsh_autosuggest_strategy_ai() {
context_str+="\"$(_zsh_autosuggest_strategy_ai_json_escape "$line")\""
done
# Build JSON request body
local system_prompt="You are a shell command auto-completion engine. Given the user's partial command, working directory, and recent history, predict the complete command. Reply ONLY with the complete command. No explanations, no markdown, no quotes."
local user_message="Working directory: $PWD\nRecent history: [$context_str]\nPartial command: $buffer"
# Determine prompt mode (empty vs non-empty buffer)
local system_prompt user_message temperature
if [[ -z "$buffer" ]]; then
# Empty buffer: predict next command
system_prompt="You are a shell command prediction engine. Based on the working directory, directory contents, git status, and recent history, suggest the single most likely next command the user wants to run. Reply ONLY with the complete command. No explanations, no markdown, no quotes."
temperature="0.5"
user_message="Working directory: $PWD"
[[ -n "${env_context[dir_contents]}" ]] && user_message+="\nDirectory contents: ${env_context[dir_contents]}"
[[ -n "${env_context[git_branch]}" ]] && user_message+="\nGit branch: ${env_context[git_branch]}"
[[ -n "${env_context[git_status]}" ]] && user_message+="\nGit changes: ${env_context[git_status]}"
user_message+="\nRecent history: [$context_str]"
else
# Non-empty buffer: complete partial command
system_prompt="You are a shell command auto-completion engine. Given the user's partial command, working directory, and recent history, predict the complete command. Reply ONLY with the complete command. No explanations, no markdown, no quotes."
temperature="0.3"
user_message="Working directory: $PWD"
[[ -n "${env_context[dir_contents]}" ]] && user_message+="\nDirectory contents: ${env_context[dir_contents]}"
[[ -n "${env_context[git_branch]}" ]] && user_message+="\nGit branch: ${env_context[git_branch]}"
[[ -n "${env_context[git_status]}" ]] && user_message+="\nGit changes: ${env_context[git_status]}"
user_message+="\nRecent history: [$context_str]"
user_message+="\nPartial command: $buffer"
fi
# Build JSON request body
local json_body
json_body=$(printf '{
"model": "%s",
"messages": [
{"role": "system", "content": "%s"},
{"role": "system", "content": "%s"},
{"role": "user", "content": "%s"}
],
"temperature": 0.3,
"temperature": %s,
"max_tokens": 100
}' \
"${ZSH_AUTOSUGGEST_AI_MODEL:-gpt-3.5-turbo}" \
"$(_zsh_autosuggest_strategy_ai_json_escape "$system_prompt")" \
"$(_zsh_autosuggest_strategy_ai_json_escape "$user_message")")
"$(_zsh_autosuggest_strategy_ai_json_escape "$user_message")" \
"$temperature")
# Make API request
local endpoint="${ZSH_AUTOSUGGEST_AI_ENDPOINT:-https://api.openai.com/v1/chat/completions}"

View file

@ -13,7 +13,7 @@ _zsh_autosuggest_disable() {
_zsh_autosuggest_enable() {
unset _ZSH_AUTOSUGGEST_DISABLED
if (( $#BUFFER )); then
if (( $#BUFFER )) || (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )); then
_zsh_autosuggest_fetch
fi
}
@ -77,6 +77,8 @@ _zsh_autosuggest_modify() {
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
_zsh_autosuggest_fetch
fi
return $retval
@ -99,7 +101,7 @@ _zsh_autosuggest_suggest() {
local suggestion="$1"
if [[ -n "$suggestion" ]] && (( $#BUFFER )); then
if [[ -n "$suggestion" ]] && { (( $#BUFFER )) || (( ${+ZSH_AUTOSUGGEST_ALLOW_EMPTY_BUFFER} )); }; then
POSTDISPLAY="${suggestion#$BUFFER}"
else
POSTDISPLAY=