fix(ai): prevent async suggestion duplication

- Add buffer validation to check suggestion starts with buffer
- Discard stale suggestions that don't match current content

Fixes text duplication bug when accepting async suggestions after
buffer changes during suggestion fetch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Frad LEE 2026-02-12 17:01:15 +08:00
commit 55cd4ec23d
3 changed files with 85 additions and 1 deletions

52
.claude/git.local.md Normal file
View file

@ -0,0 +1,52 @@
---
enabled: true
# Commit Message Conventions
scopes:
- ai
- widget
- strategy
- spec
- integration
- option
- docs
- ci
types:
- feat
- fix
- docs
- refactor
- test
- chore
- perf
- style
# Branch Naming Conventions
branch_prefixes:
feature: feature/*
fix: fix/*
hotfix: hotfix/*
refactor: refactor/*
docs: docs/*
# .gitignore Generation Defaults
gitignore:
os: [macos, linux, windows]
languages: [ruby, shell]
frameworks: []
tools: [rspec, github-actions]
---
# Project-Specific Git Settings
This file configures the `@git/` plugin for this project. The settings above in the YAML frontmatter define valid scopes, types, and branch naming conventions that the plugin will enforce.
## Usage
- **Scopes**: When creating a commit with `/commit`, choose from the defined `scopes`.
- **Branching**: When creating a new branch via the `git` skill, use the defined `branch_prefixes`.
- **Gitignore**: When running `/gitignore` without arguments, the technologies listed above will be used as defaults.
## Additional Guidelines
- Always run tests before committing.
- Ensure linting passes before pushing.
- Reference issue numbers in commit footers (e.g., `Closes #123`).
- Use `BREAKING CHANGE:` prefix in the body for breaking changes.

View file

@ -405,3 +405,29 @@ EOFCURL
wait_for { session.content }.to eq('temp 0.3')
end
end
context 'suggestion buffer validation' do
let(:options) { ["ZSH_AUTOSUGGEST_AI_API_KEY=test-key", "ZSH_AUTOSUGGEST_STRATEGY=(ai)"] }
context 'when suggestion does not match current buffer' do
let(:before_sourcing) do
-> {
session.run_command('curl() {
if [[ "$*" == *"max-time"* ]]; then
cat <<EOFCURL
{"choices":[{"message":{"content":"git status --long"}}]}
200
EOFCURL
fi
}')
}
end
it 'discards suggestion that does not match buffer' do
# Suggestion is "git status --long" but buffer is unrelated
session.send_string('ls')
wait_for { session.content }.to_not match(/git status/)
end
end
end
end

View file

@ -105,7 +105,13 @@ _zsh_autosuggest_suggest() {
local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-1}"
if [[ -n "$suggestion" ]] && { (( $#BUFFER )) || (( min_input == 0 )); }; then
POSTDISPLAY="${suggestion#$BUFFER}"
# Only extract suffix if suggestion starts with buffer
if [[ "$suggestion" == "$BUFFER"* ]]; then
POSTDISPLAY="${suggestion#$BUFFER}"
else
# Suggestion doesn't match current buffer, discard it
POSTDISPLAY=
fi
else
POSTDISPLAY=
fi