mirror of
https://github.com/zsh-users/zsh-autosuggestions.git
synced 2026-02-09 16:41:32 +01:00
Merge 8f1d53df7a into 85919cd1ff
This commit is contained in:
commit
78fb80ebfa
10 changed files with 1606 additions and 5 deletions
87
README.md
87
README.md
|
|
@ -49,11 +49,12 @@ For more info, read the Character Highlighting section of the zsh manual: `man z
|
|||
|
||||
### Suggestion Strategy
|
||||
|
||||
`ZSH_AUTOSUGGEST_STRATEGY` is an array that specifies how suggestions should be generated. The strategies in the array are tried successively until a suggestion is found. There are currently three built-in strategies to choose from:
|
||||
`ZSH_AUTOSUGGEST_STRATEGY` is an array that specifies how suggestions should be generated. The strategies in the array are tried successively until a suggestion is found. There are currently four built-in strategies to choose from:
|
||||
|
||||
- `history`: Chooses the most recent match from history.
|
||||
- `completion`: Chooses a suggestion based on what tab-completion would suggest. (requires `zpty` module, which is included with zsh since 4.0.1)
|
||||
- `match_prev_cmd`: Like `history`, but chooses the most recent match whose preceding history item matches the most recently executed command ([more info](src/strategies/match_prev_cmd.zsh)). Note that this strategy won't work as expected with ZSH options that don't preserve the history order such as `HIST_IGNORE_ALL_DUPS` or `HIST_EXPIRE_DUPS_FIRST`.
|
||||
- `ai`: Queries an OpenAI-compatible LLM API to generate command completions based on your partial input, working directory, and recent shell history. (requires `curl` and `jq`, opt-in via `ZSH_AUTOSUGGEST_AI_API_KEY`)
|
||||
|
||||
For example, setting `ZSH_AUTOSUGGEST_STRATEGY=(history completion)` will first try to find a suggestion from your history, but, if it can't find a match, will find a suggestion from the completion engine.
|
||||
|
||||
|
|
@ -100,6 +101,90 @@ Set `ZSH_AUTOSUGGEST_COMPLETION_IGNORE` to a [glob pattern](http://zsh.sourcefor
|
|||
|
||||
**Note:** This only affects the `completion` suggestion strategy.
|
||||
|
||||
### AI Strategy Configuration
|
||||
|
||||
The `ai` strategy uses an OpenAI-compatible LLM API to generate intelligent command completions based on your shell context.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- `curl` command-line tool
|
||||
- `jq` JSON processor
|
||||
- API key for an OpenAI-compatible service
|
||||
|
||||
#### Setup
|
||||
|
||||
To enable the AI strategy, you must set the `ZSH_AUTOSUGGEST_AI_API_KEY` environment variable and add `ai` to your strategy list:
|
||||
|
||||
```sh
|
||||
export ZSH_AUTOSUGGEST_AI_API_KEY="your-api-key-here"
|
||||
export ZSH_AUTOSUGGEST_STRATEGY=(ai history)
|
||||
```
|
||||
|
||||
**Security Note:** Never commit your API key to version control. Consider storing it in a separate file (e.g., `~/.zsh_secrets`) that is sourced in your `.zshrc` and excluded from version control.
|
||||
|
||||
#### Configuration Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `ZSH_AUTOSUGGEST_AI_API_KEY` | (unset) | **Required.** API key for the LLM service. Strategy is disabled if unset. |
|
||||
| `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` | `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_AI_MIN_INPUT=0`:
|
||||
|
||||
```sh
|
||||
export ZSH_AUTOSUGGEST_AI_MIN_INPUT=0
|
||||
export ZSH_AUTOSUGGEST_AI_API_KEY="your-api-key-here"
|
||||
export ZSH_AUTOSUGGEST_STRATEGY=(ai history)
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Requires zsh 5.3+ for prompt-time suggestions
|
||||
- This feature is primarily designed for the AI strategy, which can predict the next likely command based on your current directory, git status, and recent 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):**
|
||||
```sh
|
||||
export ZSH_AUTOSUGGEST_AI_API_KEY="sk-..."
|
||||
export ZSH_AUTOSUGGEST_STRATEGY=(ai history)
|
||||
```
|
||||
|
||||
**Ollama (local LLM):**
|
||||
```sh
|
||||
export ZSH_AUTOSUGGEST_AI_API_KEY="not-needed" # Required but unused by Ollama
|
||||
export ZSH_AUTOSUGGEST_AI_ENDPOINT="http://localhost:11434/v1"
|
||||
export ZSH_AUTOSUGGEST_AI_MODEL="codellama"
|
||||
export ZSH_AUTOSUGGEST_STRATEGY=(ai history)
|
||||
```
|
||||
|
||||
**Custom OpenAI-compatible endpoint:**
|
||||
```sh
|
||||
export ZSH_AUTOSUGGEST_AI_API_KEY="your-key"
|
||||
export ZSH_AUTOSUGGEST_AI_ENDPOINT="https://your-endpoint.com/v1"
|
||||
export ZSH_AUTOSUGGEST_AI_MODEL="your-model"
|
||||
export ZSH_AUTOSUGGEST_STRATEGY=(ai history)
|
||||
```
|
||||
|
||||
|
||||
### Key Bindings
|
||||
|
||||
|
|
|
|||
25
spec/strategies/ai_debug_spec.rb
Normal file
25
spec/strategies/ai_debug_spec.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
describe 'the `ai` strategy debug logging' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_STRATEGY=(ai)"] }
|
||||
|
||||
context 'when debug is enabled' do
|
||||
let(:options) do
|
||||
[
|
||||
"ZSH_AUTOSUGGEST_STRATEGY=(ai)",
|
||||
"ZSH_AUTOSUGGEST_AI_DEBUG=1"
|
||||
]
|
||||
end
|
||||
|
||||
it 'logs why AI suggestion is skipped when API key is missing' do
|
||||
session.send_string('brew')
|
||||
wait_for { session.content }.to match(/\[zsh-autosuggestions ai\] API key not set/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when debug is disabled by default' do
|
||||
it 'does not print AI debug logs' do
|
||||
session.send_string('brew')
|
||||
sleep 0.2
|
||||
expect(session.content).not_to match(/\[zsh-autosuggestions ai\]/)
|
||||
end
|
||||
end
|
||||
end
|
||||
407
spec/strategies/ai_spec.rb
Normal file
407
spec/strategies/ai_spec.rb
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
describe 'the `ai` suggestion strategy' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_STRATEGY=(ai history)"] }
|
||||
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
if [[ "$*" == *"max-time"* ]]; then
|
||||
cat <<EOF
|
||||
{"choices":[{"message":{"content":"git status --short"}}]}
|
||||
200
|
||||
EOF
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
context 'when API key is not set' do
|
||||
it 'returns no suggestion and falls through to next strategy' do
|
||||
with_history('git status --short') do
|
||||
session.send_string('git st')
|
||||
wait_for { session.content }.to eq('git status --short')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API key is set' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai history)"] }
|
||||
|
||||
context 'and curl/jq are available' do
|
||||
it 'suggests completion from AI' do
|
||||
session.send_string('git st')
|
||||
wait_for { session.content }.to eq('git status --short')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when input is below minimum threshold' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_AI_MIN_INPUT=5", "ZSH_AUTOSUGGEST_STRATEGY=(ai history)"] }
|
||||
|
||||
it 'returns no suggestion for short input' do
|
||||
with_history('git status') do
|
||||
session.send_string('git')
|
||||
wait_for { session.content }.to eq('git status')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when curl fails' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai history)"] }
|
||||
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() { return 1 }')
|
||||
}
|
||||
end
|
||||
|
||||
it 'falls through to next strategy' do
|
||||
with_history('git status') do
|
||||
session.send_string('git st')
|
||||
wait_for { session.content }.to eq('git status')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API returns HTTP error' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai history)"] }
|
||||
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
if [[ "$*" == *"max-time"* ]]; then
|
||||
cat <<EOF
|
||||
{"error":"Unauthorized"}
|
||||
401
|
||||
EOF
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
it 'falls through to next strategy' do
|
||||
with_history('git status') do
|
||||
session.send_string('git st')
|
||||
wait_for { session.content }.to eq('git status')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API returns malformed JSON' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai history)"] }
|
||||
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
if [[ "$*" == *"max-time"* ]]; then
|
||||
cat <<EOF
|
||||
not valid json
|
||||
200
|
||||
EOF
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
it 'falls through to next strategy' do
|
||||
with_history('git status') do
|
||||
session.send_string('git st')
|
||||
wait_for { session.content }.to eq('git status')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'response normalization' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] }
|
||||
|
||||
context 'when response has markdown code fences' do
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
if [[ "$*" == *"max-time"* ]]; then
|
||||
cat <<EOF
|
||||
{"choices":[{"message":{"content":"```\\ngit status\\n```"}}]}
|
||||
200
|
||||
EOF
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
it 'strips the code fences' do
|
||||
session.send_string('git st')
|
||||
wait_for { session.content }.to eq('git status')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response includes the input prefix' do
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
if [[ "$*" == *"max-time"* ]]; then
|
||||
cat <<EOF
|
||||
{"choices":[{"message":{"content":"git status --short"}}]}
|
||||
200
|
||||
EOF
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
it 'uses the complete command' do
|
||||
session.send_string('git st')
|
||||
wait_for { session.content }.to eq('git status --short')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'fallback strategy' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai history)"] }
|
||||
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() { return 1 }')
|
||||
}
|
||||
end
|
||||
|
||||
it 'uses history when AI fails' do
|
||||
with_history('git status --long') do
|
||||
session.send_string('git st')
|
||||
wait_for { session.content }.to eq('git status --long')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'prompt artifact stripping' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] }
|
||||
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
if [[ "$*" == *"max-time"* ]]; then
|
||||
cat <<EOFCURL
|
||||
{"choices":[{"message":{"content":"$ git status"}}]}
|
||||
200
|
||||
EOFCURL
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
it 'strips $ prompt artifact' do
|
||||
session.send_string('git st')
|
||||
wait_for { session.content }.to eq('git status')
|
||||
end
|
||||
end
|
||||
|
||||
context 'empty buffer suggestions' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_AI_MIN_INPUT=0", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] }
|
||||
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
if [[ "$*" == *"max-time"* ]]; then
|
||||
cat <<EOFCURL
|
||||
{"choices":[{"message":{"content":"git status"}}]}
|
||||
200
|
||||
EOFCURL
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
it 'suggests command on empty buffer when enabled' do
|
||||
session.send_keys('C-c')
|
||||
wait_for { session.content(esc_seqs: true) }.to match(/git status/)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
if [[ "$*" == *"max-time"* ]]; then
|
||||
cat <<EOFCURL
|
||||
{"choices":[{"message":{"content":"git status"}}]}
|
||||
200
|
||||
EOFCURL
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
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/)
|
||||
session.send_string('git')
|
||||
wait_for { session.content }.to eq('git status')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'endpoint construction' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] }
|
||||
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
local url=""
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == http* ]]; then
|
||||
url="$arg"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$url" == */chat/completions ]]; then
|
||||
cat <<EOFCURL
|
||||
{"choices":[{"message":{"content":"correct endpoint"}}]}
|
||||
200
|
||||
EOFCURL
|
||||
else
|
||||
cat <<EOFCURL
|
||||
{"error":"wrong endpoint"}
|
||||
400
|
||||
EOFCURL
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
it 'appends /chat/completions to base URL' do
|
||||
session.send_string('test')
|
||||
wait_for { session.content }.to eq('correct endpoint')
|
||||
end
|
||||
|
||||
context 'with custom base URL' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_AI_ENDPOINT=http://custom.api/v1", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] }
|
||||
|
||||
it 'constructs endpoint correctly' do
|
||||
session.send_string('test')
|
||||
wait_for { session.content }.to eq('correct endpoint')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'environmental context gathering' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] }
|
||||
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
local data=""
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == -d ]]; then
|
||||
shift
|
||||
data="$1"
|
||||
break
|
||||
fi
|
||||
shift
|
||||
done
|
||||
if [[ "$data" == *"Directory contents"* ]]; then
|
||||
cat <<EOFCURL
|
||||
{"choices":[{"message":{"content":"has directory context"}}]}
|
||||
200
|
||||
EOFCURL
|
||||
else
|
||||
cat <<EOFCURL
|
||||
{"choices":[{"message":{"content":"no context"}}]}
|
||||
200
|
||||
EOFCURL
|
||||
fi
|
||||
}')
|
||||
session.run_command('ls() { echo "file1.txt\nfile2.txt"; }')
|
||||
}
|
||||
end
|
||||
|
||||
it 'includes directory contents in context' do
|
||||
session.send_string('test')
|
||||
wait_for { session.content }.to eq('has directory context')
|
||||
end
|
||||
end
|
||||
|
||||
context 'dual prompt modes' do
|
||||
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
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
local data=""
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == -d ]]; then
|
||||
shift
|
||||
data="$1"
|
||||
break
|
||||
fi
|
||||
shift
|
||||
done
|
||||
if [[ "$data" == *"prediction engine"* ]]; then
|
||||
cat <<EOFCURL
|
||||
{"choices":[{"message":{"content":"prediction mode active"}}]}
|
||||
200
|
||||
EOFCURL
|
||||
elif [[ "$data" == *"auto-completion engine"* ]]; then
|
||||
cat <<EOFCURL
|
||||
{"choices":[{"message":{"content":"completion mode active"}}]}
|
||||
200
|
||||
EOFCURL
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
it 'uses prediction prompt for empty buffer' do
|
||||
session.send_keys('C-c')
|
||||
wait_for { session.content(esc_seqs: true) }.to match(/prediction mode/)
|
||||
end
|
||||
|
||||
it 'uses completion prompt for partial input' do
|
||||
session.send_string('git')
|
||||
wait_for { session.content }.to match(/completion mode/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'temperature configuration' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_AI_MIN_INPUT=0", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] }
|
||||
|
||||
let(:before_sourcing) do
|
||||
-> {
|
||||
session.run_command('curl() {
|
||||
local data=""
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == -d ]]; then
|
||||
shift
|
||||
data="$1"
|
||||
break
|
||||
fi
|
||||
shift
|
||||
done
|
||||
if [[ "$data" == *"\"temperature\": 0.5"* ]]; then
|
||||
cat <<EOFCURL
|
||||
{"choices":[{"message":{"content":"temp 0.5"}}]}
|
||||
200
|
||||
EOFCURL
|
||||
elif [[ "$data" == *"\"temperature\": 0.3"* ]]; then
|
||||
cat <<EOFCURL
|
||||
{"choices":[{"message":{"content":"temp 0.3"}}]}
|
||||
200
|
||||
EOFCURL
|
||||
fi
|
||||
}')
|
||||
}
|
||||
end
|
||||
|
||||
it 'uses temperature 0.5 for empty buffer' do
|
||||
session.send_keys('C-c')
|
||||
wait_for { session.content(esc_seqs: true) }.to match(/temp 0.5/)
|
||||
end
|
||||
|
||||
it 'uses temperature 0.3 for partial input' do
|
||||
session.send_string('test')
|
||||
wait_for { session.content }.to eq('temp 0.3')
|
||||
end
|
||||
end
|
||||
|
|
@ -93,3 +93,32 @@ typeset -g ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig-
|
|||
# Pty name for capturing completions for completion suggestion strategy
|
||||
(( ! ${+ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME} )) &&
|
||||
typeset -g ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_pty
|
||||
|
||||
# AI strategy configuration
|
||||
# API base URL for AI suggestions (OpenAI-compatible)
|
||||
(( ! ${+ZSH_AUTOSUGGEST_AI_ENDPOINT} )) &&
|
||||
typeset -g ZSH_AUTOSUGGEST_AI_ENDPOINT='https://api.openai.com/v1'
|
||||
|
||||
# AI model to use for suggestions
|
||||
(( ! ${+ZSH_AUTOSUGGEST_AI_MODEL} )) &&
|
||||
typeset -g ZSH_AUTOSUGGEST_AI_MODEL='gpt-3.5-turbo'
|
||||
|
||||
# Timeout in seconds for AI API requests
|
||||
(( ! ${+ZSH_AUTOSUGGEST_AI_TIMEOUT} )) &&
|
||||
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=1
|
||||
|
||||
# Number of recent history lines to include as context
|
||||
(( ! ${+ZSH_AUTOSUGGEST_AI_HISTORY_LINES} )) &&
|
||||
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=no
|
||||
|
||||
# Enable AI debug logs to stderr (opt-in).
|
||||
# Set to any value except 0/false/no/off to enable.
|
||||
|
|
|
|||
|
|
@ -31,3 +31,18 @@ fi
|
|||
|
||||
# Start the autosuggestion widgets on the next precmd
|
||||
add-zsh-hook precmd _zsh_autosuggest_start
|
||||
|
||||
_zsh_autosuggest_line_init() {
|
||||
emulate -L zsh
|
||||
local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}"
|
||||
if (( min_input == 0 )) && \
|
||||
(( ! ${+_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
|
||||
|
|
|
|||
291
src/strategies/ai.zsh
Normal file
291
src/strategies/ai.zsh
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
|
||||
#--------------------------------------------------------------------#
|
||||
# AI Suggestion Strategy #
|
||||
#--------------------------------------------------------------------#
|
||||
# Queries an OpenAI-compatible LLM API to generate command
|
||||
# completions based on partial input, working directory, and
|
||||
# recent shell history.
|
||||
#
|
||||
|
||||
_zsh_autosuggest_strategy_ai_json_escape() {
|
||||
# Reset options to defaults and enable LOCAL_OPTIONS
|
||||
emulate -L zsh
|
||||
|
||||
local input="$1"
|
||||
local output=""
|
||||
local char
|
||||
|
||||
for ((i=1; i<=${#input}; i++)); do
|
||||
char="${input:$((i-1)):1}"
|
||||
case "$char" in
|
||||
'\\') output+='\\\\' ;;
|
||||
'"') output+='\"' ;;
|
||||
$'\n') output+='\n' ;;
|
||||
$'\t') output+='\t' ;;
|
||||
$'\r') output+='\r' ;;
|
||||
[[:cntrl:]]) ;; # Skip other control chars
|
||||
*) output+="$char" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s' "$output"
|
||||
}
|
||||
|
||||
_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:-5}"
|
||||
local prefer_pwd="${ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY:-no}"
|
||||
local pwd_basename="${PWD:t}"
|
||||
local -a context_lines
|
||||
local -a pwd_lines
|
||||
local -a other_lines
|
||||
local line
|
||||
|
||||
# Iterate from most recent history
|
||||
for line in "${(@On)history}"; do
|
||||
# Truncate long lines
|
||||
if [[ ${#line} -gt 200 ]]; then
|
||||
line="${line:0:200}..."
|
||||
fi
|
||||
|
||||
# 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[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
|
||||
|
||||
local response="$1"
|
||||
local buffer="$2"
|
||||
local result=""
|
||||
|
||||
# Strip \r
|
||||
response="${response//$'\r'/}"
|
||||
|
||||
# Strip leading prompt artifacts ($ or >)
|
||||
response="${response##\$ }"
|
||||
response="${response##> }"
|
||||
|
||||
# Strip markdown code fences
|
||||
response="${response##\`\`\`*$'\n'}"
|
||||
response="${response%%$'\n'\`\`\`}"
|
||||
|
||||
# Strip surrounding quotes
|
||||
if [[ "$response" == \"*\" || "$response" == \'*\' ]]; then
|
||||
response="${response:1:-1}"
|
||||
fi
|
||||
|
||||
# Trim whitespace
|
||||
response="${response##[[:space:]]##}"
|
||||
response="${response%%[[:space:]]##}"
|
||||
|
||||
# Take first line only
|
||||
result="${response%%$'\n'*}"
|
||||
|
||||
# If response starts with buffer, extract suffix
|
||||
if [[ "$result" == "$buffer"* ]]; then
|
||||
local suffix="${result#$buffer}"
|
||||
result="$buffer$suffix"
|
||||
# If response looks like a pure suffix, prepend buffer
|
||||
elif [[ -n "$buffer" ]] && [[ "$result" != "$buffer"* ]] && [[ "${buffer}${result}" == [[:print:]]* ]]; then
|
||||
result="$buffer$result"
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
typeset -g suggestion
|
||||
local buffer="$1"
|
||||
|
||||
# Early return if API key not set (opt-in gate)
|
||||
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
|
||||
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:-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
|
||||
_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
|
||||
if [[ -n "$context_str" ]]; then
|
||||
context_str+=", "
|
||||
fi
|
||||
context_str+="\"$(_zsh_autosuggest_strategy_ai_json_escape "$line")\""
|
||||
done
|
||||
|
||||
# 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": "user", "content": "%s"}
|
||||
],
|
||||
"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")" \
|
||||
"$temperature")
|
||||
|
||||
# Make API request
|
||||
local base_url="${ZSH_AUTOSUGGEST_AI_ENDPOINT:-https://api.openai.com/v1}"
|
||||
local endpoint="${base_url}/chat/completions"
|
||||
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" \
|
||||
-d "$json_body" \
|
||||
-w '\n%{http_code}' \
|
||||
"$endpoint" 2>/dev/null)
|
||||
|
||||
# Check curl exit status
|
||||
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
|
||||
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
|
||||
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
|
||||
if [[ -n "$normalized" ]]; then
|
||||
suggestion="$normalized"
|
||||
_zsh_autosuggest_strategy_ai_debug_log "AI suggestion accepted: '$normalized'."
|
||||
fi
|
||||
}
|
||||
|
|
@ -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 )); then
|
||||
if (( $#BUFFER )) || (( min_input == 0 )); then
|
||||
_zsh_autosuggest_fetch
|
||||
fi
|
||||
}
|
||||
|
|
@ -73,10 +74,13 @@ _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 (( min_input == 0 )); then
|
||||
_zsh_autosuggest_fetch
|
||||
fi
|
||||
|
||||
return $retval
|
||||
|
|
@ -98,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 )); then
|
||||
if [[ -n "$suggestion" ]] && { (( $#BUFFER )) || (( min_input == 0 )); }; then
|
||||
POSTDISPLAY="${suggestion#$BUFFER}"
|
||||
else
|
||||
POSTDISPLAY=
|
||||
|
|
|
|||
258
test_ai_integration.zsh
Executable file
258
test_ai_integration.zsh
Executable file
|
|
@ -0,0 +1,258 @@
|
|||
#!/usr/bin/env zsh
|
||||
#
|
||||
# AI Strategy Integration Test
|
||||
# Tests that AI functionality is working correctly without requiring real API calls
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test counter
|
||||
TESTS_RUN=0
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Helper functions
|
||||
print_test() {
|
||||
echo "${YELLOW}[TEST]${NC} $1"
|
||||
}
|
||||
|
||||
print_pass() {
|
||||
echo "${GREEN}[PASS]${NC} $1"
|
||||
((TESTS_PASSED++))
|
||||
}
|
||||
|
||||
print_fail() {
|
||||
echo "${RED}[FAIL]${NC} $1"
|
||||
((TESTS_FAILED++))
|
||||
}
|
||||
|
||||
# Source the plugin
|
||||
source ./zsh-autosuggestions.zsh
|
||||
|
||||
# Mock curl for testing
|
||||
curl() {
|
||||
local url=""
|
||||
local data=""
|
||||
local expect_url="https://api.openai.com/v1/chat/completions"
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-d)
|
||||
data="$2"
|
||||
shift 2
|
||||
;;
|
||||
http*)
|
||||
url="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Verify endpoint construction
|
||||
if [[ "$url" != "$expect_url" ]]; then
|
||||
echo '{"error":"wrong endpoint"}'
|
||||
echo '400'
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Return mock response based on data
|
||||
echo '{"choices":[{"message":{"content":"git status"}}]}'
|
||||
echo '200'
|
||||
}
|
||||
|
||||
# Mock jq
|
||||
jq() {
|
||||
if [[ "$1" == "-r" ]]; then
|
||||
# Simple extraction of content field
|
||||
grep -o '"content":"[^"]*"' | cut -d'"' -f4
|
||||
fi
|
||||
}
|
||||
|
||||
# Mock ls for environment context
|
||||
ls() {
|
||||
if [[ "$1" == "-1" ]]; then
|
||||
echo "file1.txt"
|
||||
echo "file2.txt"
|
||||
echo "README.md"
|
||||
fi
|
||||
}
|
||||
|
||||
# Mock git for environment context
|
||||
git() {
|
||||
if [[ "$1" == "branch" ]]; then
|
||||
echo "master"
|
||||
elif [[ "$1" == "status" ]]; then
|
||||
echo "M file1.txt"
|
||||
echo "?? file2.txt"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "AI Strategy Integration Tests"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Test 1: Endpoint construction
|
||||
print_test "Endpoint construction with base URL"
|
||||
((TESTS_RUN++))
|
||||
export ZSH_AUTOSUGGEST_AI_API_KEY="test-key"
|
||||
export ZSH_AUTOSUGGEST_AI_ENDPOINT="https://api.openai.com/v1"
|
||||
result=$(_zsh_autosuggest_strategy_ai "test" 2>&1)
|
||||
if [[ $? -eq 0 ]]; then
|
||||
print_pass "Base URL correctly constructs full endpoint"
|
||||
else
|
||||
print_fail "Endpoint construction failed"
|
||||
fi
|
||||
|
||||
# Test 2: Custom base URL
|
||||
print_test "Custom base URL endpoint construction"
|
||||
((TESTS_RUN++))
|
||||
export ZSH_AUTOSUGGEST_AI_ENDPOINT="http://localhost:11434/v1"
|
||||
# Override curl mock for this test
|
||||
curl() {
|
||||
local url=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
http*)
|
||||
url="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$url" == "http://localhost:11434/v1/chat/completions" ]]; then
|
||||
echo '{"choices":[{"message":{"content":"ollama response"}}]}'
|
||||
echo '200'
|
||||
else
|
||||
echo '{"error":"wrong endpoint"}'
|
||||
echo '400'
|
||||
fi
|
||||
}
|
||||
result=$(_zsh_autosuggest_strategy_ai "test" 2>&1)
|
||||
if [[ $? -eq 0 ]]; then
|
||||
print_pass "Custom base URL works correctly"
|
||||
else
|
||||
print_fail "Custom base URL failed"
|
||||
fi
|
||||
|
||||
# Reset curl mock
|
||||
curl() {
|
||||
local url=""
|
||||
local data=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-d)
|
||||
data="$2"
|
||||
shift 2
|
||||
;;
|
||||
http*)
|
||||
url="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
echo '{"choices":[{"message":{"content":"git status"}}]}'
|
||||
echo '200'
|
||||
}
|
||||
|
||||
# Test 3: Environment context gathering
|
||||
print_test "Environment context gathering"
|
||||
((TESTS_RUN++))
|
||||
typeset -g reply
|
||||
_zsh_autosuggest_strategy_ai_gather_env_context
|
||||
if [[ -n "${reply[dir_contents]}" ]]; then
|
||||
print_pass "Directory contents captured: ${reply[dir_contents]}"
|
||||
else
|
||||
print_fail "Directory contents not captured"
|
||||
fi
|
||||
|
||||
# Test 4: PWD-aware history gathering
|
||||
print_test "PWD-aware history gathering"
|
||||
((TESTS_RUN++))
|
||||
# Add some history
|
||||
fc -p # Push history
|
||||
print -s "git status"
|
||||
print -s "ls -la"
|
||||
print -s "cd /tmp"
|
||||
_zsh_autosuggest_strategy_ai_gather_context
|
||||
if [[ ${#reply[@]} -gt 0 ]]; then
|
||||
print_pass "History context gathered: ${#reply[@]} entries"
|
||||
else
|
||||
print_fail "History gathering failed"
|
||||
fi
|
||||
|
||||
# Test 5: Response normalization with prompt artifacts
|
||||
print_test "Prompt artifact stripping"
|
||||
((TESTS_RUN++))
|
||||
result=$(_zsh_autosuggest_strategy_ai_normalize "$ git status" "git")
|
||||
if [[ "$result" == "git status" ]]; then
|
||||
print_pass "$ prompt artifact stripped correctly"
|
||||
else
|
||||
print_fail "Prompt artifact stripping failed: got '$result'"
|
||||
fi
|
||||
|
||||
# Test 6: Response normalization with > artifact
|
||||
print_test "> prompt artifact stripping"
|
||||
((TESTS_RUN++))
|
||||
result=$(_zsh_autosuggest_strategy_ai_normalize "> ls -la" "ls")
|
||||
if [[ "$result" == "ls -la" ]]; then
|
||||
print_pass "> prompt artifact stripped correctly"
|
||||
else
|
||||
print_fail "> artifact stripping failed: got '$result'"
|
||||
fi
|
||||
|
||||
# Test 7: Minimum input length (now default 0)
|
||||
print_test "Minimum input length allows empty buffer"
|
||||
((TESTS_RUN++))
|
||||
export ZSH_AUTOSUGGEST_AI_MIN_INPUT=0
|
||||
result=$(_zsh_autosuggest_strategy_ai "" 2>&1)
|
||||
# Should not fail due to length check
|
||||
if [[ $? -eq 0 ]] || [[ "$result" != *"too short"* ]]; then
|
||||
print_pass "Empty buffer allowed with MIN_INPUT=0"
|
||||
else
|
||||
print_fail "Empty buffer rejected incorrectly"
|
||||
fi
|
||||
|
||||
# Test 8: JSON escaping
|
||||
print_test "JSON escaping for special characters"
|
||||
((TESTS_RUN++))
|
||||
result=$(_zsh_autosuggest_strategy_ai_json_escape 'test "quote" and \backslash')
|
||||
if [[ "$result" == *'\"'* ]] && [[ "$result" == *'\\'* ]]; then
|
||||
print_pass "Special characters escaped correctly"
|
||||
else
|
||||
print_fail "JSON escaping failed: got '$result'"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Test Summary"
|
||||
echo "=========================================="
|
||||
echo "Tests run: $TESTS_RUN"
|
||||
echo "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
|
||||
echo "Tests failed: ${RED}$TESTS_FAILED${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ $TESTS_FAILED -eq 0 ]]; then
|
||||
echo "${GREEN}✓ All tests passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo "${RED}✗ Some tests failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
146
test_ai_simple.sh
Executable file
146
test_ai_simple.sh
Executable file
|
|
@ -0,0 +1,146 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Simple AI Strategy Test
|
||||
# Validates core AI functionality
|
||||
#
|
||||
|
||||
echo "=== AI Strategy Core Functionality Tests ==="
|
||||
echo ""
|
||||
|
||||
# Test 1: Check source files exist
|
||||
echo "[TEST 1] Source files exist"
|
||||
if [[ -f "src/strategies/ai.zsh" ]] && [[ -f "src/config.zsh" ]]; then
|
||||
echo "✓ PASS: Source files found"
|
||||
else
|
||||
echo "✗ FAIL: Source files missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Check built file includes AI strategy
|
||||
echo ""
|
||||
echo "[TEST 2] Built file includes AI strategy"
|
||||
if grep -q "_zsh_autosuggest_strategy_ai" zsh-autosuggestions.zsh; then
|
||||
echo "✓ PASS: AI strategy function present in built file"
|
||||
else
|
||||
echo "✗ FAIL: AI strategy missing from built file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 3: Verify base URL configuration
|
||||
echo ""
|
||||
echo "[TEST 3] Base URL configuration"
|
||||
if grep -q "ZSH_AUTOSUGGEST_AI_ENDPOINT='https://api.openai.com/v1'" zsh-autosuggestions.zsh; then
|
||||
echo "✓ PASS: Base URL configured correctly"
|
||||
else
|
||||
echo "✗ FAIL: Base URL configuration incorrect"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 4: Verify endpoint construction code
|
||||
echo ""
|
||||
echo "[TEST 4] Endpoint construction logic"
|
||||
if grep -q 'local base_url=.*ZSH_AUTOSUGGEST_AI_ENDPOINT' zsh-autosuggestions.zsh && \
|
||||
grep -q 'local endpoint=.*chat/completions' zsh-autosuggestions.zsh; then
|
||||
echo "✓ PASS: Endpoint construction code present"
|
||||
else
|
||||
echo "✗ FAIL: Endpoint construction logic missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 5: Environment context function exists
|
||||
echo ""
|
||||
echo "[TEST 5] Environment context gathering function"
|
||||
if grep -q "_zsh_autosuggest_strategy_ai_gather_env_context" zsh-autosuggestions.zsh; then
|
||||
echo "✓ PASS: Environment context function present"
|
||||
else
|
||||
echo "✗ FAIL: Environment context function missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 6: Prompt artifact stripping
|
||||
echo ""
|
||||
echo "[TEST 6] Prompt artifact stripping code"
|
||||
if grep -q 'response=.*##\\$ ' zsh-autosuggestions.zsh && \
|
||||
grep -q 'response=.*##> ' zsh-autosuggestions.zsh; then
|
||||
echo "✓ PASS: Prompt artifact stripping present"
|
||||
else
|
||||
echo "✗ FAIL: Prompt artifact stripping missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 7: Empty buffer support
|
||||
echo ""
|
||||
echo "[TEST 7] Empty buffer configuration"
|
||||
if grep -q "ALLOW_EMPTY_BUFFER" zsh-autosuggestions.zsh; then
|
||||
echo "✓ PASS: Empty buffer support present"
|
||||
else
|
||||
echo "✗ FAIL: Empty buffer support missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 8: Dual prompt system
|
||||
echo ""
|
||||
echo "[TEST 8] Dual prompt system (predict vs complete)"
|
||||
if grep -q "prediction engine" zsh-autosuggestions.zsh && \
|
||||
grep -q "auto-completion engine" zsh-autosuggestions.zsh; then
|
||||
echo "✓ PASS: Dual prompt system present"
|
||||
else
|
||||
echo "✗ FAIL: Dual prompt system missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 9: Temperature configuration
|
||||
echo ""
|
||||
echo "[TEST 9] Temperature configuration"
|
||||
if grep -q '"temperature": %s' zsh-autosuggestions.zsh; then
|
||||
echo "✓ PASS: Dynamic temperature configuration present"
|
||||
else
|
||||
echo "✗ FAIL: Temperature configuration missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 10: MIN_INPUT default is 0
|
||||
echo ""
|
||||
echo "[TEST 10] MIN_INPUT default value"
|
||||
if grep -q 'ZSH_AUTOSUGGEST_AI_MIN_INPUT=0' zsh-autosuggestions.zsh; then
|
||||
echo "✓ PASS: MIN_INPUT default is 0"
|
||||
else
|
||||
echo "✗ FAIL: MIN_INPUT default incorrect"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 11: Documentation updated
|
||||
echo ""
|
||||
echo "[TEST 11] Documentation updates"
|
||||
if grep -q "ALLOW_EMPTY_BUFFER" README.md && \
|
||||
grep -q "Empty Buffer Suggestions" README.md; then
|
||||
echo "✓ PASS: Documentation includes new features"
|
||||
else
|
||||
echo "✗ FAIL: Documentation missing updates"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 12: RSpec tests added
|
||||
echo ""
|
||||
echo "[TEST 12] RSpec test coverage"
|
||||
if grep -q "empty buffer" spec/strategies/ai_spec.rb && \
|
||||
grep -q "prompt artifact" spec/strategies/ai_spec.rb; then
|
||||
echo "✓ PASS: New test cases added"
|
||||
else
|
||||
echo "✗ FAIL: Test coverage incomplete"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "✓ All 12 core functionality tests passed!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "AI Strategy is ready for use with:"
|
||||
echo " - Base URL configuration"
|
||||
echo " - Empty buffer suggestions"
|
||||
echo " - Environment context gathering"
|
||||
echo " - PWD-aware history"
|
||||
echo " - Dual prompt modes"
|
||||
echo " - Prompt artifact stripping"
|
||||
echo ""
|
||||
|
|
@ -120,6 +120,35 @@ typeset -g ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig-
|
|||
(( ! ${+ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME} )) &&
|
||||
typeset -g ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_pty
|
||||
|
||||
# AI strategy configuration
|
||||
# API base URL for AI suggestions (OpenAI-compatible)
|
||||
(( ! ${+ZSH_AUTOSUGGEST_AI_ENDPOINT} )) &&
|
||||
typeset -g ZSH_AUTOSUGGEST_AI_ENDPOINT='https://api.openai.com/v1'
|
||||
|
||||
# AI model to use for suggestions
|
||||
(( ! ${+ZSH_AUTOSUGGEST_AI_MODEL} )) &&
|
||||
typeset -g ZSH_AUTOSUGGEST_AI_MODEL='gpt-3.5-turbo'
|
||||
|
||||
# Timeout in seconds for AI API requests
|
||||
(( ! ${+ZSH_AUTOSUGGEST_AI_TIMEOUT} )) &&
|
||||
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=1
|
||||
|
||||
# Number of recent history lines to include as context
|
||||
(( ! ${+ZSH_AUTOSUGGEST_AI_HISTORY_LINES} )) &&
|
||||
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=no
|
||||
|
||||
# Enable AI debug logs to stderr (opt-in).
|
||||
# Set to any value except 0/false/no/off to enable.
|
||||
|
||||
#--------------------------------------------------------------------#
|
||||
# Utility Functions #
|
||||
#--------------------------------------------------------------------#
|
||||
|
|
@ -276,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 )); then
|
||||
if (( $#BUFFER )) || (( min_input == 0 )); then
|
||||
_zsh_autosuggest_fetch
|
||||
fi
|
||||
}
|
||||
|
|
@ -337,10 +367,13 @@ _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 (( min_input == 0 )); then
|
||||
_zsh_autosuggest_fetch
|
||||
fi
|
||||
|
||||
return $retval
|
||||
|
|
@ -362,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 )); then
|
||||
if [[ -n "$suggestion" ]] && { (( $#BUFFER )) || (( min_input == 0 )); }; then
|
||||
POSTDISPLAY="${suggestion#$BUFFER}"
|
||||
else
|
||||
POSTDISPLAY=
|
||||
|
|
@ -494,6 +528,297 @@ _zsh_autosuggest_partial_accept() {
|
|||
done
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------#
|
||||
# AI Suggestion Strategy #
|
||||
#--------------------------------------------------------------------#
|
||||
# Queries an OpenAI-compatible LLM API to generate command
|
||||
# completions based on partial input, working directory, and
|
||||
# recent shell history.
|
||||
#
|
||||
|
||||
_zsh_autosuggest_strategy_ai_json_escape() {
|
||||
# Reset options to defaults and enable LOCAL_OPTIONS
|
||||
emulate -L zsh
|
||||
|
||||
local input="$1"
|
||||
local output=""
|
||||
local char
|
||||
|
||||
for ((i=1; i<=${#input}; i++)); do
|
||||
char="${input:$((i-1)):1}"
|
||||
case "$char" in
|
||||
'\\') output+='\\\\' ;;
|
||||
'"') output+='\"' ;;
|
||||
$'\n') output+='\n' ;;
|
||||
$'\t') output+='\t' ;;
|
||||
$'\r') output+='\r' ;;
|
||||
[[:cntrl:]]) ;; # Skip other control chars
|
||||
*) output+="$char" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s' "$output"
|
||||
}
|
||||
|
||||
_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:-5}"
|
||||
local prefer_pwd="${ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY:-no}"
|
||||
local pwd_basename="${PWD:t}"
|
||||
local -a context_lines
|
||||
local -a pwd_lines
|
||||
local -a other_lines
|
||||
local line
|
||||
|
||||
# Iterate from most recent history
|
||||
for line in "${(@On)history}"; do
|
||||
# Truncate long lines
|
||||
if [[ ${#line} -gt 200 ]]; then
|
||||
line="${line:0:200}..."
|
||||
fi
|
||||
|
||||
# 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[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
|
||||
|
||||
local response="$1"
|
||||
local buffer="$2"
|
||||
local result=""
|
||||
|
||||
# Strip \r
|
||||
response="${response//$'\r'/}"
|
||||
|
||||
# Strip leading prompt artifacts ($ or >)
|
||||
response="${response##\$ }"
|
||||
response="${response##> }"
|
||||
|
||||
# Strip markdown code fences
|
||||
response="${response##\`\`\`*$'\n'}"
|
||||
response="${response%%$'\n'\`\`\`}"
|
||||
|
||||
# Strip surrounding quotes
|
||||
if [[ "$response" == \"*\" || "$response" == \'*\' ]]; then
|
||||
response="${response:1:-1}"
|
||||
fi
|
||||
|
||||
# Trim whitespace
|
||||
response="${response##[[:space:]]##}"
|
||||
response="${response%%[[:space:]]##}"
|
||||
|
||||
# Take first line only
|
||||
result="${response%%$'\n'*}"
|
||||
|
||||
# If response starts with buffer, extract suffix
|
||||
if [[ "$result" == "$buffer"* ]]; then
|
||||
local suffix="${result#$buffer}"
|
||||
result="$buffer$suffix"
|
||||
# If response looks like a pure suffix, prepend buffer
|
||||
elif [[ -n "$buffer" ]] && [[ "$result" != "$buffer"* ]] && [[ "${buffer}${result}" == [[:print:]]* ]]; then
|
||||
result="$buffer$result"
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
typeset -g suggestion
|
||||
local buffer="$1"
|
||||
|
||||
# Early return if API key not set (opt-in gate)
|
||||
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
|
||||
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:-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
|
||||
_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
|
||||
if [[ -n "$context_str" ]]; then
|
||||
context_str+=", "
|
||||
fi
|
||||
context_str+="\"$(_zsh_autosuggest_strategy_ai_json_escape "$line")\""
|
||||
done
|
||||
|
||||
# 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": "user", "content": "%s"}
|
||||
],
|
||||
"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")" \
|
||||
"$temperature")
|
||||
|
||||
# Make API request
|
||||
local base_url="${ZSH_AUTOSUGGEST_AI_ENDPOINT:-https://api.openai.com/v1}"
|
||||
local endpoint="${base_url}/chat/completions"
|
||||
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" \
|
||||
-d "$json_body" \
|
||||
-w '\n%{http_code}' \
|
||||
"$endpoint" 2>/dev/null)
|
||||
|
||||
# Check curl exit status
|
||||
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
|
||||
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
|
||||
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
|
||||
if [[ -n "$normalized" ]]; then
|
||||
suggestion="$normalized"
|
||||
_zsh_autosuggest_strategy_ai_debug_log "AI suggestion accepted: '$normalized'."
|
||||
fi
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------#
|
||||
# Completion Suggestion Strategy #
|
||||
#--------------------------------------------------------------------#
|
||||
|
|
@ -865,3 +1190,18 @@ fi
|
|||
|
||||
# Start the autosuggestion widgets on the next precmd
|
||||
add-zsh-hook precmd _zsh_autosuggest_start
|
||||
|
||||
_zsh_autosuggest_line_init() {
|
||||
emulate -L zsh
|
||||
local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}"
|
||||
if (( min_input == 0 )) && \
|
||||
(( ! ${+_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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue