gitbasher 5.1.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +16 -40
- package/dist/gitb +481 -177
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to gitbasher are generated automatically by [semantic-release](https://github.com/semantic-release/semantic-release) from [Conventional Commits](https://www.conventionalcommits.org/) on `main`. The project follows [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [5.2.1](https://github.com/maxbolgarin/gitbasher/compare/v5.2.0...v5.2.1) (2026-07-11)
|
|
6
|
+
|
|
7
|
+
### 🐛 Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **commit:** gate AI split by scope count and stop discarding near-complete groupings ([90becec](https://github.com/maxbolgarin/gitbasher/commit/90becec0f49a7a3e57e57e11a71b6909884ab337))
|
|
10
|
+
|
|
11
|
+
## [5.2.0](https://github.com/maxbolgarin/gitbasher/compare/v5.1.0...v5.2.0) (2026-07-07)
|
|
12
|
+
|
|
13
|
+
### 🚀 Features
|
|
14
|
+
|
|
15
|
+
* **ai:** one model per provider with live pickers, reasoning control, and setup safety checks ([a9c6935](https://github.com/maxbolgarin/gitbasher/commit/a9c6935297e84b777c8428203dae7003ef93aa38))
|
|
16
|
+
|
|
17
|
+
### 🐛 Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **ai:** extract bare scope from full type(scope) prefixes ([18da7e4](https://github.com/maxbolgarin/gitbasher/commit/18da7e4648a0dda2aec2453305bcdf95110feea7))
|
|
20
|
+
* **config:** move values to global scope instead of shadowing them locally ([648ef37](https://github.com/maxbolgarin/gitbasher/commit/648ef3735dfd6c1aa248369d51bdb57ad42c3353))
|
|
21
|
+
|
|
22
|
+
### 📚 Documentation
|
|
23
|
+
|
|
24
|
+
* describe per-provider AI models, live model menus, and reasoning behavior ([dab3cef](https://github.com/maxbolgarin/gitbasher/commit/dab3cef479d619e59e69b6d4f3a75514e7b6d00d))
|
|
25
|
+
|
|
26
|
+
### 🚨 Tests
|
|
27
|
+
|
|
28
|
+
* keep [@test](https://github.com/test) descriptions ASCII for bats under bash 3.2 ([1e3fb50](https://github.com/maxbolgarin/gitbasher/commit/1e3fb50071d927b8475178ffdae68afcf7ebc19a))
|
|
29
|
+
|
|
5
30
|
## [5.1.0](https://github.com/maxbolgarin/gitbasher/compare/v5.0.1...v5.1.0) (2026-07-03)
|
|
6
31
|
|
|
7
32
|
### 🚀 Features
|
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ Every command has a short alias (`gitb c`, `gitb p`, `gitb pu`, `gitb b`, `gitb
|
|
|
111
111
|
- **AI commit messages** — `gitb c ai` writes the message from your diff. OpenRouter (Gemini/Claude/GPT/…), OpenAI direct, or **fully local via Ollama** — no key, no network, no data leaves your machine.
|
|
112
112
|
- **Safer git** — push/pull detect conflicts up front, `undo` rolls back commit/amend/merge/rebase/stash, `reset` is interactive with a preview.
|
|
113
113
|
- **Whole workflows, not just commands** — `sync`, `wip`, `branch newd`, `merge to-main`, `squash` chain the steps you'd otherwise do by hand.
|
|
114
|
-
- **One file, no deps** — pure bash for every git operation; only the AI features need `jq` and `curl`. Drop the binary anywhere on `PATH` and go. **
|
|
114
|
+
- **One file, no deps** — pure bash for every git operation; only the AI features need `jq` and `curl`. Drop the binary anywhere on `PATH` and go. **940+ BATS tests** cover sanitization, git ops, and branch logic.
|
|
115
115
|
|
|
116
116
|
<p align="center">
|
|
117
117
|
<img src=".github/push.gif" width="640" alt="gitb push demo" />
|
|
@@ -253,50 +253,26 @@ export GITB_AI_API_KEY='sk-...'
|
|
|
253
253
|
<details>
|
|
254
254
|
<summary><b>Default models per provider</b> (click to expand)</summary>
|
|
255
255
|
|
|
256
|
-
|
|
256
|
+
One model per provider, used for every task (message generation, subjects, feature grouping). Defaults (July 2026), picked speed-first:
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
| Provider | Default model | Why |
|
|
259
|
+
|----------|---------------|-----|
|
|
260
|
+
| `openrouter` | `google/gemini-3.5-flash` | Current flash generation, ~0.8s median on a commit-sized prompt, good prose |
|
|
261
|
+
| `openai` | `gpt-5.4-mini` | As fast as nano (~0.8s) with strictly better quality, ~$0.75 / $4.50 per M |
|
|
262
|
+
| `ollama` | `qwen3:8b` | Best small instruction-follower; most stable structured output; ~5 GB on disk |
|
|
263
|
+
| `claude` | `haiku` | CLI startup dominates latency, so the fastest model wins; alias tracks the current generation |
|
|
259
264
|
|
|
260
|
-
|
|
261
|
-
|------|---------------|-----|
|
|
262
|
-
| `simple` (one-line message) | `google/gemini-3.1-flash-lite` | Fastest model measured (~0.6s), cheapest |
|
|
263
|
-
| `subject` (after manual type/scope) | `google/gemini-3.1-flash-lite` | Short structured output, instant |
|
|
264
|
-
| `full` (header + body) | `google/gemini-3.5-flash` | Same speed class, better prose |
|
|
265
|
-
| `grouping` (atomic-split mapping) | `google/gemini-3.5-flash` | Fast + strict structured output |
|
|
265
|
+
Other good picks: `gpt-5.4-nano` (budget, high volume), `llama3.3:8b` / `qwen2.5-coder:7b` (local), `sonnet` (better prose on the claude provider, noticeably slower).
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
The cloud defaults are hybrid reasoning models that think at "medium" effort out of the box — pure latency for a one-line commit message. gitbasher controls this per request: message generation runs at the provider's reasoning floor (`minimal` on OpenRouter, `none` on OpenAI — measured ~2× faster with identical output), while feature grouping and squash planning run at `low` effort, where a bit of real thinking helps multi-file structure decisions. No configuration needed.
|
|
268
268
|
|
|
269
|
-
|
|
270
|
-
|------|---------------|-----|
|
|
271
|
-
| All tasks | `gpt-5.4-mini` | Measured as fast as nano (~0.8s) with strictly better quality, ~$0.75 / $4.50 per M |
|
|
269
|
+
`gitb cfg model` shows a **live menu**: the week's most-used and newest text models on OpenRouter (public API, server-side ranking), or the newest chat models visible to your OpenAI key. A typed id is validated against the live catalog (typos die at config time, with an override for brand-new models), and the chosen model is test-driven with one live request before the step completes — a model that can't complete a request never reaches your commits.
|
|
272
270
|
|
|
273
|
-
|
|
271
|
+
The model is remembered **per provider** — switching providers never carries a model across (an OpenRouter slug means nothing to the claude CLI), and switching back restores your previous choice, same as API keys:
|
|
274
272
|
|
|
275
|
-
**Ollama** — fully local
|
|
276
|
-
|
|
277
|
-
| Task | Default model | Why |
|
|
278
|
-
|------|---------------|-----|
|
|
279
|
-
| All tasks | `qwen3:8b` | Best small instruction-follower among 7/8B models; most stable structured output (rarely drops fields in TSV); ~5 GB on disk, ~25 tok/s on a consumer laptop with GPU |
|
|
280
|
-
|
|
281
|
-
Other strong local picks: `llama3.3:8b` (general-purpose), `qwen2.5-coder:7b` (code-heavy diffs).
|
|
282
|
-
|
|
283
|
-
**Claude Code CLI** — local `claude -p`
|
|
284
|
-
|
|
285
|
-
| Task | Default model | Why |
|
|
286
|
-
|------|---------------|-----|
|
|
287
|
-
| `simple` / `subject` | `haiku` | Fastest and cheapest for one-line output |
|
|
288
|
-
| `full` / `grouping` | `sonnet` | Better prose and strict structured output |
|
|
289
|
-
|
|
290
|
-
The alias slugs (`haiku`, `sonnet`, `opus`) always resolve to the current model generation; full ids like `claude-haiku-4-5` work too.
|
|
291
|
-
|
|
292
|
-
Override per task or globally:
|
|
293
273
|
```bash
|
|
294
|
-
gitb cfg model
|
|
295
|
-
git config gitbasher.ai-model
|
|
296
|
-
git config gitbasher.ai-model-simple <model_id> # per-task
|
|
297
|
-
git config gitbasher.ai-model-subject <model_id>
|
|
298
|
-
git config gitbasher.ai-model-full <model_id>
|
|
299
|
-
git config gitbasher.ai-model-grouping <model_id>
|
|
274
|
+
gitb cfg model # interactive, stores for the active provider
|
|
275
|
+
git config gitbasher.ai-model-<provider> <model_id> # direct, e.g. gitbasher.ai-model-claude sonnet
|
|
300
276
|
```
|
|
301
277
|
|
|
302
278
|
</details>
|
|
@@ -905,7 +881,7 @@ Overview-first diffs built for the gitbasher workflow — no flag memorization.
|
|
|
905
881
|
| `gitbasher.ai-api-key-<provider>` | `gitb cfg ai` | Per-provider AI API key (or `GITB_AI_API_KEY_<PROVIDER>` env); legacy `gitbasher.ai-api-key` is read as a fallback |
|
|
906
882
|
| `gitbasher.ai-provider` | `gitb cfg provider` | `openrouter` (default), `openai`, `ollama`, or `claude` |
|
|
907
883
|
| `gitbasher.ai-base-url` | `git config` | Custom OpenAI-compatible endpoint (LiteLLM, vLLM, remote Ollama) |
|
|
908
|
-
| `gitbasher.ai-model
|
|
884
|
+
| `gitbasher.ai-model-<provider>` | `gitb cfg model` | Model for that provider (one model for all tasks); legacy `gitbasher.ai-model` is read as a fallback |
|
|
909
885
|
| `gitbasher.ai-proxy` | `gitb cfg proxy` | HTTP proxy for AI calls |
|
|
910
886
|
| `gitbasher.ai-ollama-host` | `git config gitbasher.ai-ollama-host <url>` | Ollama server URL (default `http://localhost:11434`) |
|
|
911
887
|
| `gitbasher.ai-timeout` | `git config gitbasher.ai-timeout <seconds>` | AI request timeout in seconds (default `60`, `300` for ollama/claude) |
|
|
@@ -1095,7 +1071,7 @@ PRs welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the dev setup, BATS te
|
|
|
1095
1071
|
|
|
1096
1072
|
```bash
|
|
1097
1073
|
make build # rebuild dist/gitb
|
|
1098
|
-
make test # run all
|
|
1074
|
+
make test # run all 940+ tests
|
|
1099
1075
|
make test-file FILE=test_sanitization.bats
|
|
1100
1076
|
```
|
|
1101
1077
|
|
package/dist/gitb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -o pipefail
|
|
3
|
-
GITBASHER_VERSION="5.1
|
|
3
|
+
GITBASHER_VERSION="5.2.1"
|
|
4
4
|
if [ "$1" == "init" ] || [ "$1" == "i" ]; then
|
|
5
5
|
git init
|
|
6
6
|
fi
|
|
@@ -875,11 +875,22 @@ function print_ai_configuration {
|
|
|
875
875
|
fi
|
|
876
876
|
local ai_model=$(get_ai_model)
|
|
877
877
|
if [ -n "$ai_model" ]; then
|
|
878
|
-
|
|
878
|
+
local model_key="gitbasher.ai-model-${ai_provider}"
|
|
879
|
+
if [ "$(config_source "$model_key")" = "default" ]; then
|
|
880
|
+
model_key="gitbasher.ai-model"
|
|
881
|
+
fi
|
|
882
|
+
echo -e "\tAI model:\t${GREEN}$ai_model${ENDCOLOR} $(config_source_tag "$model_key")"
|
|
879
883
|
else
|
|
880
|
-
echo -e "\tAI
|
|
881
|
-
|
|
882
|
-
|
|
884
|
+
echo -e "\tAI model:\t${GREEN}$(get_ai_default_model)${ENDCOLOR} ${GRAY}(default)${ENDCOLOR}"
|
|
885
|
+
fi
|
|
886
|
+
local other_models=""
|
|
887
|
+
while IFS= read -r prov; do
|
|
888
|
+
[ -z "$prov" ] && continue
|
|
889
|
+
[ "$prov" = "$ai_provider" ] && continue
|
|
890
|
+
other_models="${other_models:+${other_models}, }${prov}"
|
|
891
|
+
done < <(list_providers_with_model)
|
|
892
|
+
if [ -n "$other_models" ]; then
|
|
893
|
+
echo -e "\t\t\t${GRAY}also set for: ${other_models}${ENDCOLOR}"
|
|
883
894
|
fi
|
|
884
895
|
}
|
|
885
896
|
function print_configuration {
|
|
@@ -1551,6 +1562,9 @@ function set_config_value {
|
|
|
1551
1562
|
git config --local "$1" "$2"
|
|
1552
1563
|
else
|
|
1553
1564
|
git config --global "$1" "$2"
|
|
1565
|
+
if [ "$GITBASHER_NO_REPO" != "true" ]; then
|
|
1566
|
+
git config --local --unset "$1" 2>/dev/null || true
|
|
1567
|
+
fi
|
|
1554
1568
|
fi
|
|
1555
1569
|
printf '%s\n' "$2"
|
|
1556
1570
|
}
|
|
@@ -1696,9 +1710,9 @@ readonly OPENAI_API_URL="https://api.openai.com/v1/chat/completions"
|
|
|
1696
1710
|
readonly AI_DEFAULT_OLLAMA_HOST="http://localhost:11434"
|
|
1697
1711
|
readonly AI_DEFAULT_PROVIDER="openrouter"
|
|
1698
1712
|
readonly AI_TEMPERATURE="0.3"
|
|
1699
|
-
readonly AI_MAX_TOKENS_SUBJECT=
|
|
1700
|
-
readonly AI_MAX_TOKENS_SIMPLE=
|
|
1701
|
-
readonly AI_MAX_TOKENS_FULL=
|
|
1713
|
+
readonly AI_MAX_TOKENS_SUBJECT=512
|
|
1714
|
+
readonly AI_MAX_TOKENS_SIMPLE=1024
|
|
1715
|
+
readonly AI_MAX_TOKENS_FULL=2048
|
|
1702
1716
|
function get_ai_api_key {
|
|
1703
1717
|
local provider=$(get_ai_provider)
|
|
1704
1718
|
local provider_upper
|
|
@@ -1834,82 +1848,69 @@ function ai_provider_requires_api_key {
|
|
|
1834
1848
|
esac
|
|
1835
1849
|
}
|
|
1836
1850
|
function get_ai_model {
|
|
1851
|
+
local provider=$(get_ai_provider)
|
|
1852
|
+
local provider_model
|
|
1853
|
+
provider_model=$(get_config_value "gitbasher.ai-model-${provider}" "")
|
|
1854
|
+
if [ -n "$provider_model" ]; then
|
|
1855
|
+
echo "$provider_model"
|
|
1856
|
+
return
|
|
1857
|
+
fi
|
|
1837
1858
|
get_config_value gitbasher.ai-model ""
|
|
1838
1859
|
}
|
|
1839
1860
|
function set_ai_model {
|
|
1840
|
-
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
readonly AI_DEFAULT_MODEL_GROUPING_OLLAMA="qwen3:8b"
|
|
1854
|
-
readonly AI_DEFAULT_MODEL_SIMPLE_CLAUDE="haiku"
|
|
1855
|
-
readonly AI_DEFAULT_MODEL_SUBJECT_CLAUDE="haiku"
|
|
1856
|
-
readonly AI_DEFAULT_MODEL_FULL_CLAUDE="sonnet"
|
|
1857
|
-
readonly AI_DEFAULT_MODEL_GROUPING_CLAUDE="sonnet"
|
|
1858
|
-
function get_ai_model_for {
|
|
1859
|
-
local task="$1"
|
|
1860
|
-
local task_model
|
|
1861
|
-
task_model=$(get_config_value "gitbasher.ai-model-$task" "")
|
|
1862
|
-
if [ -n "$task_model" ]; then
|
|
1863
|
-
echo "$task_model"
|
|
1864
|
-
return
|
|
1861
|
+
local provider=$(get_ai_provider)
|
|
1862
|
+
set_config_value "gitbasher.ai-model-${provider}" "$1" "$2"
|
|
1863
|
+
}
|
|
1864
|
+
function migrate_legacy_ai_model_to {
|
|
1865
|
+
local target_provider="$1"
|
|
1866
|
+
local migrated=""
|
|
1867
|
+
local local_legacy=$(git config --local --get gitbasher.ai-model 2>/dev/null)
|
|
1868
|
+
if [ -n "$local_legacy" ]; then
|
|
1869
|
+
if ! git config --local --get "gitbasher.ai-model-${target_provider}" >/dev/null 2>&1; then
|
|
1870
|
+
git config --local "gitbasher.ai-model-${target_provider}" "$local_legacy"
|
|
1871
|
+
migrated="local"
|
|
1872
|
+
fi
|
|
1873
|
+
git config --local --unset gitbasher.ai-model 2>/dev/null || true
|
|
1865
1874
|
fi
|
|
1866
|
-
local
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1875
|
+
local global_legacy=$(git config --global --get gitbasher.ai-model 2>/dev/null)
|
|
1876
|
+
if [ -n "$global_legacy" ]; then
|
|
1877
|
+
if ! git config --global --get "gitbasher.ai-model-${target_provider}" >/dev/null 2>&1; then
|
|
1878
|
+
git config --global "gitbasher.ai-model-${target_provider}" "$global_legacy"
|
|
1879
|
+
migrated="${migrated:+$migrated, }global"
|
|
1880
|
+
fi
|
|
1881
|
+
git config --global --unset gitbasher.ai-model 2>/dev/null || true
|
|
1871
1882
|
fi
|
|
1883
|
+
if [ -n "$migrated" ]; then
|
|
1884
|
+
echo -e "${GRAY}↻ Kept model for provider '${target_provider}' (${migrated})${ENDCOLOR}" >&2
|
|
1885
|
+
fi
|
|
1886
|
+
}
|
|
1887
|
+
function list_providers_with_model {
|
|
1888
|
+
{
|
|
1889
|
+
git config --local --get-regexp '^gitbasher\.ai-model-' 2>/dev/null
|
|
1890
|
+
git config --global --get-regexp '^gitbasher\.ai-model-' 2>/dev/null
|
|
1891
|
+
} | awk '{print $1}' | sed 's/^gitbasher\.ai-model-//' \
|
|
1892
|
+
| grep -E '^(openrouter|openai|ollama|claude)$' | sort -u
|
|
1893
|
+
}
|
|
1894
|
+
readonly AI_DEFAULT_MODEL_OPENROUTER="google/gemini-3.5-flash"
|
|
1895
|
+
readonly AI_DEFAULT_MODEL_OPENAI="gpt-5.4-mini"
|
|
1896
|
+
readonly AI_DEFAULT_MODEL_OLLAMA="qwen3:8b"
|
|
1897
|
+
readonly AI_DEFAULT_MODEL_CLAUDE="haiku"
|
|
1898
|
+
function get_ai_default_model {
|
|
1872
1899
|
case "$(get_ai_provider)" in
|
|
1873
|
-
openai)
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
full) echo "$AI_DEFAULT_MODEL_FULL_OPENAI" ;;
|
|
1878
|
-
grouping) echo "$AI_DEFAULT_MODEL_GROUPING_OPENAI" ;;
|
|
1879
|
-
*) echo "$AI_DEFAULT_MODEL_SIMPLE_OPENAI" ;;
|
|
1880
|
-
esac
|
|
1881
|
-
;;
|
|
1882
|
-
ollama)
|
|
1883
|
-
case "$task" in
|
|
1884
|
-
simple) echo "$AI_DEFAULT_MODEL_SIMPLE_OLLAMA" ;;
|
|
1885
|
-
subject) echo "$AI_DEFAULT_MODEL_SUBJECT_OLLAMA" ;;
|
|
1886
|
-
full) echo "$AI_DEFAULT_MODEL_FULL_OLLAMA" ;;
|
|
1887
|
-
grouping) echo "$AI_DEFAULT_MODEL_GROUPING_OLLAMA" ;;
|
|
1888
|
-
*) echo "$AI_DEFAULT_MODEL_SIMPLE_OLLAMA" ;;
|
|
1889
|
-
esac
|
|
1890
|
-
;;
|
|
1891
|
-
claude)
|
|
1892
|
-
case "$task" in
|
|
1893
|
-
simple) echo "$AI_DEFAULT_MODEL_SIMPLE_CLAUDE" ;;
|
|
1894
|
-
subject) echo "$AI_DEFAULT_MODEL_SUBJECT_CLAUDE" ;;
|
|
1895
|
-
full) echo "$AI_DEFAULT_MODEL_FULL_CLAUDE" ;;
|
|
1896
|
-
grouping) echo "$AI_DEFAULT_MODEL_GROUPING_CLAUDE" ;;
|
|
1897
|
-
*) echo "$AI_DEFAULT_MODEL_SIMPLE_CLAUDE" ;;
|
|
1898
|
-
esac
|
|
1899
|
-
;;
|
|
1900
|
-
*)
|
|
1901
|
-
case "$task" in
|
|
1902
|
-
simple) echo "$AI_DEFAULT_MODEL_SIMPLE" ;;
|
|
1903
|
-
subject) echo "$AI_DEFAULT_MODEL_SUBJECT" ;;
|
|
1904
|
-
full) echo "$AI_DEFAULT_MODEL_FULL" ;;
|
|
1905
|
-
grouping) echo "$AI_DEFAULT_MODEL_GROUPING" ;;
|
|
1906
|
-
*) echo "$AI_DEFAULT_MODEL_SIMPLE" ;;
|
|
1907
|
-
esac
|
|
1908
|
-
;;
|
|
1900
|
+
openai) echo "$AI_DEFAULT_MODEL_OPENAI" ;;
|
|
1901
|
+
ollama) echo "$AI_DEFAULT_MODEL_OLLAMA" ;;
|
|
1902
|
+
claude) echo "$AI_DEFAULT_MODEL_CLAUDE" ;;
|
|
1903
|
+
*) echo "$AI_DEFAULT_MODEL_OPENROUTER" ;;
|
|
1909
1904
|
esac
|
|
1910
1905
|
}
|
|
1911
|
-
function
|
|
1912
|
-
|
|
1906
|
+
function resolve_ai_model {
|
|
1907
|
+
local model
|
|
1908
|
+
model=$(get_ai_model)
|
|
1909
|
+
if [ -n "$model" ]; then
|
|
1910
|
+
echo "$model"
|
|
1911
|
+
return
|
|
1912
|
+
fi
|
|
1913
|
+
get_ai_default_model
|
|
1913
1914
|
}
|
|
1914
1915
|
function get_ai_proxy {
|
|
1915
1916
|
get_config_value gitbasher.ai-proxy ""
|
|
@@ -2164,11 +2165,89 @@ function ollama_list_models {
|
|
|
2164
2165
|
local base resp names
|
|
2165
2166
|
base=$(ollama_api_base)
|
|
2166
2167
|
resp=$(curl -s --max-time 5 "${base}/api/tags" 2>/dev/null)
|
|
2167
|
-
[ -z "$resp" ] && return 1
|
|
2168
2168
|
names=$(printf '%s' "$resp" | _parse_ollama_model_names)
|
|
2169
|
+
if [ -z "$names" ] && command -v ollama >/dev/null 2>&1; then
|
|
2170
|
+
names=$(OLLAMA_HOST="$base" ollama list 2>/dev/null \
|
|
2171
|
+
| LC_ALL=C awk 'NR>1 && $1 != "" {print $1}')
|
|
2172
|
+
fi
|
|
2169
2173
|
[ -z "$names" ] && return 1
|
|
2170
2174
|
printf '%s\n' "$names"
|
|
2171
2175
|
}
|
|
2176
|
+
function secure_curl_get {
|
|
2177
|
+
local proxy_url="$1"
|
|
2178
|
+
local api_key="$2"
|
|
2179
|
+
local url="$3"
|
|
2180
|
+
(
|
|
2181
|
+
{ set +x; } 2>/dev/null
|
|
2182
|
+
unset HISTFILE
|
|
2183
|
+
local curl_cmd=(
|
|
2184
|
+
curl -s
|
|
2185
|
+
--connect-timeout 10
|
|
2186
|
+
--max-time 20
|
|
2187
|
+
)
|
|
2188
|
+
[ -n "$proxy_url" ] && curl_cmd+=(--proxy "$proxy_url")
|
|
2189
|
+
curl_cmd+=("$url")
|
|
2190
|
+
if [ -n "$api_key" ]; then
|
|
2191
|
+
local escaped_key="${api_key//\\/\\\\}"
|
|
2192
|
+
escaped_key="${escaped_key//\"/\\\"}"
|
|
2193
|
+
"${curl_cmd[@]}" --config - <<EOF 2>/dev/null
|
|
2194
|
+
header = "Authorization: Bearer ${escaped_key}"
|
|
2195
|
+
EOF
|
|
2196
|
+
else
|
|
2197
|
+
"${curl_cmd[@]}" 2>/dev/null
|
|
2198
|
+
fi
|
|
2199
|
+
)
|
|
2200
|
+
}
|
|
2201
|
+
function _parse_model_ids_json {
|
|
2202
|
+
if command -v jq &>/dev/null; then
|
|
2203
|
+
jq -r '.data[]?.id // empty' 2>/dev/null
|
|
2204
|
+
else
|
|
2205
|
+
LC_ALL=C grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
|
2206
|
+
| LC_ALL=C sed 's/.*"\([^"]*\)"$/\1/'
|
|
2207
|
+
fi
|
|
2208
|
+
}
|
|
2209
|
+
function openrouter_list_models {
|
|
2210
|
+
local sort_key="${1:-top-weekly}"
|
|
2211
|
+
local models_url resp ids
|
|
2212
|
+
models_url="${OPENROUTER_API_URL/chat\/completions/models}?sort=${sort_key}"
|
|
2213
|
+
resp=$(secure_curl_get "$(get_ai_proxy)" "" "$models_url")
|
|
2214
|
+
[ -z "$resp" ] && return 1
|
|
2215
|
+
ids=$(printf '%s' "$resp" | _parse_model_ids_json)
|
|
2216
|
+
[ -z "$ids" ] && return 1
|
|
2217
|
+
printf '%s\n' "$ids"
|
|
2218
|
+
}
|
|
2219
|
+
function openai_list_models {
|
|
2220
|
+
local api_key resp ids
|
|
2221
|
+
api_key=$(get_ai_api_key)
|
|
2222
|
+
[ -z "$api_key" ] && return 1
|
|
2223
|
+
local models_url="${OPENAI_API_URL/chat\/completions/models}"
|
|
2224
|
+
resp=$(secure_curl_get "$(get_ai_proxy)" "$api_key" "$models_url")
|
|
2225
|
+
[ -z "$resp" ] && return 1
|
|
2226
|
+
if command -v jq &>/dev/null; then
|
|
2227
|
+
ids=$(printf '%s' "$resp" | jq -r '.data // [] | sort_by(-(.created // 0)) | .[].id // empty' 2>/dev/null)
|
|
2228
|
+
else
|
|
2229
|
+
ids=$(printf '%s' "$resp" | _parse_model_ids_json)
|
|
2230
|
+
fi
|
|
2231
|
+
ids=$(printf '%s\n' "$ids" \
|
|
2232
|
+
| LC_ALL=C grep -E '^(gpt-|o[0-9]|chatgpt-)' \
|
|
2233
|
+
| LC_ALL=C grep -vE 'embedding|tts|whisper|audio|realtime|dall-e|image|moderation|transcribe|search|instruct')
|
|
2234
|
+
[ -z "$ids" ] && return 1
|
|
2235
|
+
printf '%s\n' "$ids"
|
|
2236
|
+
}
|
|
2237
|
+
function ai_model_smoke_check {
|
|
2238
|
+
local model out rc
|
|
2239
|
+
model=$(resolve_ai_model)
|
|
2240
|
+
echo -e "${YELLOW}Testing model '${model}' with a live request...${ENDCOLOR}"
|
|
2241
|
+
out=$(call_ai_api "Reply with exactly: OK" "ping" 64 "$model" 2>&1)
|
|
2242
|
+
rc=$?
|
|
2243
|
+
if [ "$rc" -eq 0 ] && [ -n "$out" ]; then
|
|
2244
|
+
echo -e "${GREEN}✓ Model '${model}' responded${ENDCOLOR}"
|
|
2245
|
+
return 0
|
|
2246
|
+
fi
|
|
2247
|
+
echo -e "${RED}✗ Model '${model}' did not respond${ENDCOLOR}" >&2
|
|
2248
|
+
[ -n "$out" ] && printf '%s\n' "$out" | head -4 >&2
|
|
2249
|
+
return 1
|
|
2250
|
+
}
|
|
2172
2251
|
function ai_smoke_check {
|
|
2173
2252
|
local provider
|
|
2174
2253
|
provider=$(get_ai_provider)
|
|
@@ -2268,14 +2347,17 @@ function _call_claude_cli {
|
|
|
2268
2347
|
local model="$3"
|
|
2269
2348
|
local timeout_s output rc
|
|
2270
2349
|
timeout_s=$(get_ai_timeout)
|
|
2350
|
+
local -a claude_args=(-p --model "$model" --output-format text
|
|
2351
|
+
--strict-mcp-config --mcp-config '{"mcpServers":{}}'
|
|
2352
|
+
--setting-sources "")
|
|
2271
2353
|
if command -v perl >/dev/null 2>&1; then
|
|
2272
2354
|
output=$(printf '%s\n\n%s' "$system_prompt" "$user_prompt" \
|
|
2273
|
-
| perl -e 'alarm shift; exec @ARGV' "$timeout_s" \
|
|
2274
|
-
claude
|
|
2355
|
+
| MAX_THINKING_TOKENS=0 perl -e 'alarm shift; exec @ARGV' "$timeout_s" \
|
|
2356
|
+
claude "${claude_args[@]}" 2>/dev/null)
|
|
2275
2357
|
rc=$?
|
|
2276
2358
|
else
|
|
2277
2359
|
output=$(printf '%s\n\n%s' "$system_prompt" "$user_prompt" \
|
|
2278
|
-
| claude
|
|
2360
|
+
| MAX_THINKING_TOKENS=0 claude "${claude_args[@]}" 2>/dev/null)
|
|
2279
2361
|
rc=$?
|
|
2280
2362
|
fi
|
|
2281
2363
|
if [ "$rc" -eq 142 ]; then
|
|
@@ -2294,8 +2376,9 @@ function call_ai_api {
|
|
|
2294
2376
|
local system_prompt="$1"
|
|
2295
2377
|
local user_prompt="$2"
|
|
2296
2378
|
local max_tokens="${3:-$AI_MAX_TOKENS_FULL}"
|
|
2297
|
-
local model="${4:-$(
|
|
2379
|
+
local model="${4:-$(resolve_ai_model)}"
|
|
2298
2380
|
local response_format="${5:-}"
|
|
2381
|
+
local reasoning_effort="${6:-}"
|
|
2299
2382
|
local provider=$(get_ai_provider)
|
|
2300
2383
|
local api_url=$(get_ai_api_url)
|
|
2301
2384
|
local api_key=$(get_ai_api_key)
|
|
@@ -2328,6 +2411,21 @@ function call_ai_api {
|
|
|
2328
2411
|
gpt-5*|o[0-9]*) include_temperature="" ;;
|
|
2329
2412
|
esac
|
|
2330
2413
|
fi
|
|
2414
|
+
case "$reasoning_effort" in
|
|
2415
|
+
low|medium|high|"") : ;;
|
|
2416
|
+
*) reasoning_effort="" ;; # unknown value → provider floor
|
|
2417
|
+
esac
|
|
2418
|
+
local or_reasoning="" oa_reasoning=""
|
|
2419
|
+
if [ -n "$(get_ai_base_url)" ]; then
|
|
2420
|
+
: # custom endpoint — send nothing
|
|
2421
|
+
elif [ "$provider" = "openrouter" ]; then
|
|
2422
|
+
or_reasoning="${reasoning_effort:-minimal}"
|
|
2423
|
+
elif [ "$provider" = "openai" ]; then
|
|
2424
|
+
case "$model" in
|
|
2425
|
+
gpt-5*chat*|chatgpt-*) : ;;
|
|
2426
|
+
gpt-5*|o[0-9]*) oa_reasoning="${reasoning_effort:-none}" ;;
|
|
2427
|
+
esac
|
|
2428
|
+
fi
|
|
2331
2429
|
local json_payload
|
|
2332
2430
|
if command -v jq &>/dev/null; then
|
|
2333
2431
|
json_payload=$(jq -nc \
|
|
@@ -2349,6 +2447,17 @@ function call_ai_api {
|
|
|
2349
2447
|
--argjson rf "$response_format" \
|
|
2350
2448
|
'$base + {response_format: $rf}')
|
|
2351
2449
|
fi
|
|
2450
|
+
if [ -n "$or_reasoning" ]; then
|
|
2451
|
+
json_payload=$(jq -nc \
|
|
2452
|
+
--argjson base "$json_payload" \
|
|
2453
|
+
--arg effort "$or_reasoning" \
|
|
2454
|
+
'$base + {reasoning: {effort: $effort}}')
|
|
2455
|
+
elif [ -n "$oa_reasoning" ]; then
|
|
2456
|
+
json_payload=$(jq -nc \
|
|
2457
|
+
--argjson base "$json_payload" \
|
|
2458
|
+
--arg effort "$oa_reasoning" \
|
|
2459
|
+
'$base + {reasoning_effort: $effort}')
|
|
2460
|
+
fi
|
|
2352
2461
|
else
|
|
2353
2462
|
local system_escaped=$(_json_escape_for_payload "$system_prompt")
|
|
2354
2463
|
local user_escaped=$(_json_escape_for_payload "$user_prompt")
|
|
@@ -2361,7 +2470,13 @@ function call_ai_api {
|
|
|
2361
2470
|
if [ -n "$include_temperature" ]; then
|
|
2362
2471
|
temp_field=",\"temperature\":$AI_TEMPERATURE"
|
|
2363
2472
|
fi
|
|
2364
|
-
|
|
2473
|
+
local reasoning_field=""
|
|
2474
|
+
if [ -n "$or_reasoning" ]; then
|
|
2475
|
+
reasoning_field=",\"reasoning\":{\"effort\":\"$or_reasoning\"}"
|
|
2476
|
+
elif [ -n "$oa_reasoning" ]; then
|
|
2477
|
+
reasoning_field=",\"reasoning_effort\":\"$oa_reasoning\""
|
|
2478
|
+
fi
|
|
2479
|
+
json_payload="{\"model\":\"$model_escaped\",\"messages\":[{\"role\":\"system\",\"content\":\"$system_escaped\"},{\"role\":\"user\",\"content\":\"$user_escaped\"}]${temp_field},\"${tokens_field}\":$max_tokens${rf_field}${reasoning_field}}"
|
|
2365
2480
|
fi
|
|
2366
2481
|
local proxy_url=$(get_ai_proxy)
|
|
2367
2482
|
local safe_proxy_url=""
|
|
@@ -2557,6 +2672,20 @@ function call_ai_api {
|
|
|
2557
2672
|
clear_sensitive_vars
|
|
2558
2673
|
return 1
|
|
2559
2674
|
fi
|
|
2675
|
+
local finish_reason=""
|
|
2676
|
+
if command -v jq &>/dev/null; then
|
|
2677
|
+
finish_reason=$(echo "$response" | jq -r '.choices[0].finish_reason // empty' 2>/dev/null)
|
|
2678
|
+
else
|
|
2679
|
+
case "$response" in
|
|
2680
|
+
*'"finish_reason":"length"'*|*'"finish_reason": "length"'*) finish_reason="length" ;;
|
|
2681
|
+
esac
|
|
2682
|
+
fi
|
|
2683
|
+
if [ "$finish_reason" = "length" ]; then
|
|
2684
|
+
echo -e "${RED}✗ AI response was cut off by the output-token limit.${ENDCOLOR}" >&2
|
|
2685
|
+
echo -e "${CYAN}💡 The model likely spent the budget on internal reasoning — pick a faster model with ${GREEN}gitb cfg model${ENDCOLOR}${CYAN}, or retry.${ENDCOLOR}" >&2
|
|
2686
|
+
clear_sensitive_vars
|
|
2687
|
+
return 1
|
|
2688
|
+
fi
|
|
2560
2689
|
clear_sensitive_vars
|
|
2561
2690
|
echo "$ai_response"
|
|
2562
2691
|
}
|
|
@@ -2766,13 +2895,13 @@ function generate_ai_commit_message {
|
|
|
2766
2895
|
local system_prompt user_prompt
|
|
2767
2896
|
system_prompt=$(build_ai_commit_system_prompt "$mode" "$commit_prefix")
|
|
2768
2897
|
user_prompt=$(build_ai_commit_user_prompt "$mode" "$detected_scopes" "$provided_scopes" "$rejected_messages")
|
|
2769
|
-
local max_tokens
|
|
2898
|
+
local max_tokens
|
|
2770
2899
|
case "$mode" in
|
|
2771
|
-
subject) max_tokens="$AI_MAX_TOKENS_SUBJECT"
|
|
2772
|
-
full) max_tokens="$AI_MAX_TOKENS_FULL"
|
|
2773
|
-
*) max_tokens="$AI_MAX_TOKENS_SIMPLE"
|
|
2900
|
+
subject) max_tokens="$AI_MAX_TOKENS_SUBJECT" ;;
|
|
2901
|
+
full) max_tokens="$AI_MAX_TOKENS_FULL" ;;
|
|
2902
|
+
*) max_tokens="$AI_MAX_TOKENS_SIMPLE" ;;
|
|
2774
2903
|
esac
|
|
2775
|
-
call_ai_api "$system_prompt" "$user_prompt" "$max_tokens"
|
|
2904
|
+
call_ai_api "$system_prompt" "$user_prompt" "$max_tokens"
|
|
2776
2905
|
}
|
|
2777
2906
|
function clear_sensitive_vars {
|
|
2778
2907
|
api_key=""
|
|
@@ -3053,10 +3182,10 @@ function configure_ai_provider {
|
|
|
3053
3182
|
fi
|
|
3054
3183
|
echo
|
|
3055
3184
|
echo -e "Options:"
|
|
3056
|
-
echo -e " 1. ${
|
|
3057
|
-
echo -e " 2. ${
|
|
3058
|
-
echo -e " 3. ${
|
|
3059
|
-
echo -e " 4. ${
|
|
3185
|
+
echo -e " 1. ${BOLD}openrouter${ENDCOLOR} Aggregator with hundreds of models — needs an API key"
|
|
3186
|
+
echo -e " 2. ${BOLD}openai${ENDCOLOR} OpenAI direct (GPT-5 family) — needs an API key"
|
|
3187
|
+
echo -e " 3. ${BOLD}ollama${ENDCOLOR} Local or remote models (you'll set the host) — no API key required"
|
|
3188
|
+
echo -e " 4. ${BOLD}claude${ENDCOLOR} Local Claude Code CLI (claude -p) — uses your Claude account, no API key"
|
|
3060
3189
|
echo
|
|
3061
3190
|
echo -e "Press Enter to keep the current provider, or enter 0 to reset to default (${AI_DEFAULT_PROVIDER})"
|
|
3062
3191
|
if ! read_editable_input choice "Choice (1-4): "; then
|
|
@@ -3089,8 +3218,12 @@ function configure_ai_provider {
|
|
|
3089
3218
|
;;
|
|
3090
3219
|
esac
|
|
3091
3220
|
migrate_legacy_ai_api_key_to "$current_provider"
|
|
3221
|
+
migrate_legacy_ai_model_to "$current_provider"
|
|
3222
|
+
local prev_local_provider
|
|
3223
|
+
prev_local_provider=$(git config --local --get gitbasher.ai-provider 2>/dev/null)
|
|
3092
3224
|
set_ai_provider "$new_provider"
|
|
3093
3225
|
echo -e "${GREEN}✓ Set AI provider to '${new_provider}' for '${project_name}'${ENDCOLOR}"
|
|
3226
|
+
local local_smoke_failed=""
|
|
3094
3227
|
case "$new_provider" in
|
|
3095
3228
|
ollama)
|
|
3096
3229
|
local current_host
|
|
@@ -3115,19 +3248,24 @@ function configure_ai_provider {
|
|
|
3115
3248
|
fi
|
|
3116
3249
|
echo
|
|
3117
3250
|
echo -e "${CYAN}💡 Make sure the Ollama daemon is running (${BLUE}ollama serve${CYAN}) and a model is pulled (${BLUE}ollama pull qwen3:8b${CYAN}).${ENDCOLOR}"
|
|
3118
|
-
|
|
3251
|
+
if [ -z "$_chained" ]; then
|
|
3252
|
+
echo -e "${CYAN}💡 Pick a model with ${GREEN}gitb cfg model${CYAN}.${ENDCOLOR}"
|
|
3253
|
+
fi
|
|
3119
3254
|
echo
|
|
3120
|
-
ai_smoke_check || true
|
|
3255
|
+
ai_smoke_check || local_smoke_failed="true"
|
|
3121
3256
|
;;
|
|
3122
3257
|
claude)
|
|
3123
3258
|
echo
|
|
3124
3259
|
if command -v claude >/dev/null 2>&1; then
|
|
3125
|
-
ai_smoke_check || true
|
|
3260
|
+
ai_smoke_check || local_smoke_failed="true"
|
|
3126
3261
|
else
|
|
3127
3262
|
echo -e "${RED}✗ claude CLI not found on PATH.${ENDCOLOR}"
|
|
3128
3263
|
echo -e "${CYAN}💡 Install it with ${BLUE}npm install -g @anthropic-ai/claude-code${CYAN}, then sign in once.${ENDCOLOR}"
|
|
3264
|
+
local_smoke_failed="true"
|
|
3265
|
+
fi
|
|
3266
|
+
if [ -z "$_chained" ]; then
|
|
3267
|
+
echo -e "${CYAN}💡 Pick a model with ${GREEN}gitb cfg model${CYAN} (haiku, sonnet, opus).${ENDCOLOR}"
|
|
3129
3268
|
fi
|
|
3130
|
-
echo -e "${CYAN}💡 Pick a model with ${GREEN}gitb cfg model${CYAN} (haiku, sonnet, opus).${ENDCOLOR}"
|
|
3131
3269
|
;;
|
|
3132
3270
|
*)
|
|
3133
3271
|
if [ -n "$(get_ai_api_key)" ]; then
|
|
@@ -3137,6 +3275,25 @@ function configure_ai_provider {
|
|
|
3137
3275
|
;;
|
|
3138
3276
|
esac
|
|
3139
3277
|
echo
|
|
3278
|
+
if [ -n "$local_smoke_failed" ]; then
|
|
3279
|
+
echo -e "Keep '${new_provider}' as the AI provider anyway (y/N)?"
|
|
3280
|
+
local _keep_rc=0
|
|
3281
|
+
confirm_destructive || _keep_rc=$?
|
|
3282
|
+
if [ "$_keep_rc" -ne 0 ]; then
|
|
3283
|
+
if [ -n "$prev_local_provider" ]; then
|
|
3284
|
+
git config --local gitbasher.ai-provider "$prev_local_provider"
|
|
3285
|
+
else
|
|
3286
|
+
git config --local --unset gitbasher.ai-provider 2>/dev/null || true
|
|
3287
|
+
fi
|
|
3288
|
+
echo -e "${YELLOW}Provider unchanged — still '$(get_ai_provider)'.${ENDCOLOR}"
|
|
3289
|
+
if [ -n "$_chained" ]; then
|
|
3290
|
+
if [ "$_keep_rc" -eq 2 ]; then return 130; fi
|
|
3291
|
+
return 3
|
|
3292
|
+
fi
|
|
3293
|
+
exit 1
|
|
3294
|
+
fi
|
|
3295
|
+
echo
|
|
3296
|
+
fi
|
|
3140
3297
|
if [ "$GITBASHER_NO_REPO" != "true" ]; then
|
|
3141
3298
|
echo -e "Do you want to set this provider ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
|
|
3142
3299
|
local _prov_key=""
|
|
@@ -3164,6 +3321,15 @@ function configure_ai_provider {
|
|
|
3164
3321
|
fi
|
|
3165
3322
|
fi
|
|
3166
3323
|
}
|
|
3324
|
+
function _print_model_menu_entry {
|
|
3325
|
+
local _desc=""
|
|
3326
|
+
[ -n "$4" ] && _desc=" ${GRAY}- ${4}${ENDCOLOR}"
|
|
3327
|
+
if [ "$1" = "$3" ]; then
|
|
3328
|
+
echo -e " ${GREEN}${2}. ${1}${ENDCOLOR} (current)${_desc}"
|
|
3329
|
+
else
|
|
3330
|
+
echo -e " ${2}. ${BOLD}${1}${ENDCOLOR}${_desc}"
|
|
3331
|
+
fi
|
|
3332
|
+
}
|
|
3167
3333
|
function configure_ai_model {
|
|
3168
3334
|
local _chained=""
|
|
3169
3335
|
[ "$1" = "--chained" ] && _chained="true"
|
|
@@ -3171,72 +3337,113 @@ function configure_ai_model {
|
|
|
3171
3337
|
echo
|
|
3172
3338
|
current_model=$(get_ai_model)
|
|
3173
3339
|
if [ -n "$current_model" ]; then
|
|
3174
|
-
echo -e "Current
|
|
3340
|
+
echo -e "Current model for provider '${GREEN}$(get_ai_provider)${ENDCOLOR}': ${GREEN}$current_model${ENDCOLOR}"
|
|
3175
3341
|
else
|
|
3176
|
-
echo -e "
|
|
3177
|
-
echo -e " • simple → ${GREEN}$(get_ai_model_for simple)${ENDCOLOR}"
|
|
3178
|
-
echo -e " • subject → ${GREEN}$(get_ai_model_for subject)${ENDCOLOR}"
|
|
3179
|
-
echo -e " • full → ${GREEN}$(get_ai_model_for full)${ENDCOLOR}"
|
|
3180
|
-
echo -e " • grouping → ${GREEN}$(get_ai_model_for grouping)${ENDCOLOR}"
|
|
3342
|
+
echo -e "Provider '${GREEN}$(get_ai_provider)${ENDCOLOR}' uses its default model: ${GREEN}$(get_ai_default_model)${ENDCOLOR}"
|
|
3181
3343
|
fi
|
|
3182
3344
|
echo
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3345
|
+
local -a model_menu=()
|
|
3346
|
+
local model_catalog=""
|
|
3347
|
+
local _m _i
|
|
3348
|
+
local _resolved_model
|
|
3349
|
+
_resolved_model=$(resolve_ai_model)
|
|
3188
3350
|
case "$(get_ai_provider)" in
|
|
3189
|
-
openai)
|
|
3190
|
-
echo -e " • ${BLUE}gpt-5.4-mini${ENDCOLOR} - Default: as fast as nano with better quality (~\$0.75/\$4.50 per M)"
|
|
3191
|
-
echo -e " • ${BLUE}gpt-5.4-nano${ENDCOLOR} - Budget option for high volume (~\$0.20/\$1.25 per M)"
|
|
3192
|
-
echo -e " • ${BLUE}gpt-5.5${ENDCOLOR} - Flagship, only worth it for the hardest grouping cases"
|
|
3193
|
-
;;
|
|
3194
3351
|
ollama)
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3352
|
+
while IFS= read -r _m; do
|
|
3353
|
+
[ -n "$_m" ] && model_menu+=("$_m")
|
|
3354
|
+
done < <(ollama_list_models)
|
|
3355
|
+
if [ ${#model_menu[@]} -gt 0 ]; then
|
|
3356
|
+
echo -e "Installed models on ${BLUE}$(ollama_api_base)${ENDCOLOR}:"
|
|
3357
|
+
_i=1
|
|
3358
|
+
for _m in "${model_menu[@]}"; do
|
|
3359
|
+
_print_model_menu_entry "$_m" "$_i" "$_resolved_model"
|
|
3360
|
+
_i=$((_i + 1))
|
|
3361
|
+
done
|
|
3362
|
+
else
|
|
3363
|
+
echo -e "${YELLOW}No models found on $(ollama_api_base) — is the daemon running?${ENDCOLOR}"
|
|
3364
|
+
fi
|
|
3199
3365
|
;;
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3366
|
+
openrouter)
|
|
3367
|
+
model_catalog=$(openrouter_list_models top-weekly 2>/dev/null)
|
|
3368
|
+
if [ -n "$model_catalog" ]; then
|
|
3369
|
+
_i=1
|
|
3370
|
+
echo -e "Most used on OpenRouter this week:"
|
|
3371
|
+
while IFS= read -r _m; do
|
|
3372
|
+
model_menu+=("$_m")
|
|
3373
|
+
_print_model_menu_entry "$_m" "$_i" "$_resolved_model"
|
|
3374
|
+
_i=$((_i + 1))
|
|
3375
|
+
done < <(printf '%s\n' "$model_catalog" | head -8)
|
|
3376
|
+
local _newest
|
|
3377
|
+
_newest=$(openrouter_list_models newest 2>/dev/null | head -5)
|
|
3378
|
+
if [ -n "$_newest" ]; then
|
|
3379
|
+
echo -e "Newest:"
|
|
3380
|
+
while IFS= read -r _m; do
|
|
3381
|
+
[ -z "$_m" ] && continue
|
|
3382
|
+
case " ${model_menu[*]} " in *" $_m "*) continue ;; esac
|
|
3383
|
+
model_menu+=("$_m")
|
|
3384
|
+
_print_model_menu_entry "$_m" "$_i" "$_resolved_model"
|
|
3385
|
+
_i=$((_i + 1))
|
|
3386
|
+
done <<< "$_newest"
|
|
3387
|
+
fi
|
|
3388
|
+
fi
|
|
3205
3389
|
;;
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3390
|
+
openai)
|
|
3391
|
+
model_catalog=$(openai_list_models 2>/dev/null)
|
|
3392
|
+
if [ -n "$model_catalog" ]; then
|
|
3393
|
+
echo -e "Newest chat models on your OpenAI account:"
|
|
3394
|
+
_i=1
|
|
3395
|
+
while IFS= read -r _m; do
|
|
3396
|
+
model_menu+=("$_m")
|
|
3397
|
+
_print_model_menu_entry "$_m" "$_i" "$_resolved_model"
|
|
3398
|
+
_i=$((_i + 1))
|
|
3399
|
+
done < <(printf '%s\n' "$model_catalog" \
|
|
3400
|
+
| LC_ALL=C grep -vE -- '-[0-9]{4}-[0-9]{2}-[0-9]{2}$' | head -12)
|
|
3401
|
+
fi
|
|
3212
3402
|
;;
|
|
3213
3403
|
esac
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3404
|
+
if [ ${#model_menu[@]} -eq 0 ]; then
|
|
3405
|
+
local -a _menu_desc=()
|
|
3406
|
+
case "$(get_ai_provider)" in
|
|
3407
|
+
openai)
|
|
3408
|
+
echo -e "Popular OpenAI models:"
|
|
3409
|
+
model_menu=("gpt-5.4-mini" "gpt-5.4-nano" "gpt-5.5")
|
|
3410
|
+
_menu_desc=("Default: as fast as nano with better quality (~\$0.75/\$4.50 per M)"
|
|
3411
|
+
"Budget option for high volume (~\$0.20/\$1.25 per M)"
|
|
3412
|
+
"Flagship, only worth it for the hardest grouping cases")
|
|
3413
|
+
;;
|
|
3414
|
+
ollama)
|
|
3415
|
+
echo -e "Suggested models (pull first: ${BLUE}ollama pull <id>${ENDCOLOR}):"
|
|
3416
|
+
model_menu=("qwen3:8b" "llama3.3:8b" "qwen2.5-coder:7b")
|
|
3417
|
+
_menu_desc=("Default: best small instruction-follower; most stable structured output"
|
|
3418
|
+
"Solid general-purpose alternative"
|
|
3419
|
+
"Code-focused; good when most diffs are code")
|
|
3420
|
+
;;
|
|
3421
|
+
claude)
|
|
3422
|
+
echo -e "Model aliases (resolve to the current generation; full ids like ${BLUE}claude-haiku-4-5${ENDCOLOR} also work):"
|
|
3423
|
+
model_menu=("haiku" "sonnet" "opus")
|
|
3424
|
+
_menu_desc=("Default: fastest and cheapest"
|
|
3425
|
+
"Better prose, noticeably slower"
|
|
3426
|
+
"Strongest, for the hardest cases")
|
|
3427
|
+
;;
|
|
3428
|
+
*)
|
|
3429
|
+
echo -e "Popular OpenRouter models:"
|
|
3430
|
+
model_menu=("google/gemini-3.5-flash" "google/gemini-3.1-flash-lite" "openrouter/auto" "anthropic/claude-haiku-4.5" "anthropic/claude-sonnet-5")
|
|
3431
|
+
_menu_desc=("Default: current flash generation, fast with good prose"
|
|
3432
|
+
"Fastest, cheapest (~\$0.25/\$1.50 per M)"
|
|
3433
|
+
"Auto-select best available model"
|
|
3434
|
+
"Strict instruction following"
|
|
3435
|
+
"Premium quality for the hardest cases")
|
|
3436
|
+
;;
|
|
3437
|
+
esac
|
|
3438
|
+
_i=0
|
|
3439
|
+
for _m in "${model_menu[@]}"; do
|
|
3440
|
+
_print_model_menu_entry "$_m" "$((_i + 1))" "$_resolved_model" "${_menu_desc[$_i]}"
|
|
3441
|
+
_i=$((_i + 1))
|
|
3442
|
+
done
|
|
3238
3443
|
fi
|
|
3239
|
-
echo -e "
|
|
3444
|
+
echo -e "Enter a number to select, or type a model id."
|
|
3445
|
+
echo
|
|
3446
|
+
echo -e "Press Enter to keep the current model, or 0 to reset to the default (${GREEN}$(get_ai_default_model)${ENDCOLOR})"
|
|
3240
3447
|
if ! read_editable_input model_input "Model ID: "; then
|
|
3241
3448
|
if [ -n "$_chained" ]; then return 130; fi
|
|
3242
3449
|
exit
|
|
@@ -3246,17 +3453,21 @@ function configure_ai_model {
|
|
|
3246
3453
|
exit
|
|
3247
3454
|
fi
|
|
3248
3455
|
if [ "$model_input" == "0" ]; then
|
|
3456
|
+
git config --unset "gitbasher.ai-model-$(get_ai_provider)" 2>/dev/null
|
|
3457
|
+
git config --global --unset "gitbasher.ai-model-$(get_ai_provider)" 2>/dev/null
|
|
3249
3458
|
git config --unset gitbasher.ai-model 2>/dev/null
|
|
3250
3459
|
git config --global --unset gitbasher.ai-model 2>/dev/null
|
|
3251
3460
|
echo
|
|
3252
|
-
echo -e "${GREEN}✓ Cleared
|
|
3461
|
+
echo -e "${GREEN}✓ Cleared model override — provider '$(get_ai_provider)' now uses its default ($(get_ai_default_model))${ENDCOLOR}"
|
|
3253
3462
|
if [ -n "$_chained" ]; then return 0; fi
|
|
3254
3463
|
exit
|
|
3255
3464
|
fi
|
|
3256
3465
|
echo
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3466
|
+
local picked_from_menu=""
|
|
3467
|
+
if [ ${#model_menu[@]} -gt 0 ] && [[ "$model_input" =~ ^[0-9]+$ ]]; then
|
|
3468
|
+
if [ "$model_input" -ge 1 ] && [ "$model_input" -le ${#model_menu[@]} ]; then
|
|
3469
|
+
model_input="${model_menu[$((model_input - 1))]}"
|
|
3470
|
+
picked_from_menu="true"
|
|
3260
3471
|
else
|
|
3261
3472
|
echo -e "${RED}✗ Invalid selection: ${model_input}${ENDCOLOR}" >&2
|
|
3262
3473
|
if [ -n "$_chained" ]; then return 1; fi
|
|
@@ -3269,9 +3480,41 @@ function configure_ai_model {
|
|
|
3269
3480
|
if [ -n "$_chained" ]; then return 1; fi
|
|
3270
3481
|
exit 1
|
|
3271
3482
|
fi
|
|
3483
|
+
if [ -z "$picked_from_menu" ] && [ -n "$model_catalog" ]; then
|
|
3484
|
+
if ! printf '%s\n' "$model_catalog" | LC_ALL=C grep -qxF "$model_input"; then
|
|
3485
|
+
echo -e "${YELLOW}⚠ '$model_input' is not in $(get_ai_provider)'s current model list.${ENDCOLOR}"
|
|
3486
|
+
echo -e "Save it anyway (y/N)?"
|
|
3487
|
+
if ! confirm_destructive; then
|
|
3488
|
+
echo -e "${YELLOW}Not saved. Pick a number from the list or re-check the id.${ENDCOLOR}"
|
|
3489
|
+
if [ -n "$_chained" ]; then return 1; fi
|
|
3490
|
+
exit 1
|
|
3491
|
+
fi
|
|
3492
|
+
fi
|
|
3493
|
+
fi
|
|
3494
|
+
local prev_model
|
|
3495
|
+
prev_model=$(git config --local --get "gitbasher.ai-model-$(get_ai_provider)" 2>/dev/null)
|
|
3272
3496
|
set_ai_model "$model_input"
|
|
3273
|
-
echo -e "${GREEN}✓ Set AI model to '$model_input' for '${project_name}'${ENDCOLOR}"
|
|
3497
|
+
echo -e "${GREEN}✓ Set AI model to '$model_input' for provider '$(get_ai_provider)' in '${project_name}'${ENDCOLOR}"
|
|
3274
3498
|
echo
|
|
3499
|
+
case "$(get_ai_provider)" in
|
|
3500
|
+
openrouter|openai)
|
|
3501
|
+
if ! ai_model_smoke_check; then
|
|
3502
|
+
echo -e "Keep '$model_input' anyway (y/N)?"
|
|
3503
|
+
if ! confirm_destructive; then
|
|
3504
|
+
if [ -n "$prev_model" ]; then
|
|
3505
|
+
git config "gitbasher.ai-model-$(get_ai_provider)" "$prev_model"
|
|
3506
|
+
echo -e "${YELLOW}Restored previous model '${prev_model}'.${ENDCOLOR}"
|
|
3507
|
+
else
|
|
3508
|
+
git config --unset "gitbasher.ai-model-$(get_ai_provider)" 2>/dev/null
|
|
3509
|
+
echo -e "${YELLOW}Removed it — provider '$(get_ai_provider)' uses its default ($(get_ai_default_model)).${ENDCOLOR}"
|
|
3510
|
+
fi
|
|
3511
|
+
if [ -n "$_chained" ]; then return 1; fi
|
|
3512
|
+
exit 1
|
|
3513
|
+
fi
|
|
3514
|
+
fi
|
|
3515
|
+
echo
|
|
3516
|
+
;;
|
|
3517
|
+
esac
|
|
3275
3518
|
if [ "$GITBASHER_NO_REPO" = "true" ]; then
|
|
3276
3519
|
if [ -n "$_chained" ]; then return 0; fi
|
|
3277
3520
|
exit
|
|
@@ -3281,7 +3524,7 @@ function configure_ai_model {
|
|
|
3281
3524
|
read_key _model_global || { if [ -n "$_chained" ]; then return 130; fi; _model_global="n"; }
|
|
3282
3525
|
if is_yes "$_model_global"; then
|
|
3283
3526
|
echo -e "${YELLOW}Set AI model globally${ENDCOLOR}"
|
|
3284
|
-
|
|
3527
|
+
set_ai_model "$model_input" "true"
|
|
3285
3528
|
fi
|
|
3286
3529
|
return 0
|
|
3287
3530
|
}
|
|
@@ -3292,6 +3535,11 @@ function configure_ai_wizard {
|
|
|
3292
3535
|
configure_ai_provider --chained
|
|
3293
3536
|
_rc=$?
|
|
3294
3537
|
if [ "$_rc" -eq 130 ]; then prompt_aborted; fi
|
|
3538
|
+
if [ "$_rc" -eq 3 ]; then
|
|
3539
|
+
echo
|
|
3540
|
+
echo -e "${YELLOW}AI setup stopped — nothing changed.${ENDCOLOR}"
|
|
3541
|
+
return 0
|
|
3542
|
+
fi
|
|
3295
3543
|
echo
|
|
3296
3544
|
echo -e "${YELLOW}Step 2/3 — API key${ENDCOLOR}"
|
|
3297
3545
|
echo
|
|
@@ -3325,9 +3573,9 @@ function configure_ai_proxy {
|
|
|
3325
3573
|
echo -e "Enter proxy URL to route AI requests through (useful for bypassing geo-restrictions)"
|
|
3326
3574
|
echo -e ""
|
|
3327
3575
|
echo -e "${BLUE}HTTP proxy formats:${ENDCOLOR}"
|
|
3328
|
-
echo -e " • ${
|
|
3329
|
-
echo -e " • ${
|
|
3330
|
-
echo -e " • ${
|
|
3576
|
+
echo -e " • ${BOLD}http://proxy.example.com:8080${ENDCOLOR}"
|
|
3577
|
+
echo -e " • ${BOLD}http://username:password@proxy.example.com:8080${ENDCOLOR}"
|
|
3578
|
+
echo -e " • ${BOLD}http://[2001:db8::1]:8080${ENDCOLOR} (IPv6)"
|
|
3331
3579
|
echo -e ""
|
|
3332
3580
|
echo -e "Press Enter to exit without changes, or 0 to remove the existing proxy"
|
|
3333
3581
|
read_editable_input ai_proxy_input "Proxy URL: "
|
|
@@ -3582,11 +3830,11 @@ function delete_global {
|
|
|
3582
3830
|
_delete_global_add "gitbasher.ai-ollama-host" "Ollama host" "$global_ollama_host" "$GREEN"
|
|
3583
3831
|
local global_base_url=$(git config --global --get gitbasher.ai-base-url)
|
|
3584
3832
|
_delete_global_add "gitbasher.ai-base-url" "AI base URL" "$global_base_url" "$GREEN"
|
|
3585
|
-
local
|
|
3586
|
-
|
|
3587
|
-
if [ -n "$
|
|
3588
|
-
labels+=("AI per
|
|
3589
|
-
actions+=("${
|
|
3833
|
+
local _provider_models
|
|
3834
|
+
_provider_models=$(git config --global --get-regexp '^gitbasher\.ai-model-' 2>/dev/null | awk '{print $1}' | tr '\n' '+' | sed 's/+$//')
|
|
3835
|
+
if [ -n "$_provider_models" ]; then
|
|
3836
|
+
labels+=("AI models (per provider): ${GREEN}configured${ENDCOLOR}")
|
|
3837
|
+
actions+=("${_provider_models}|AI models (per provider)")
|
|
3590
3838
|
fi
|
|
3591
3839
|
while IFS=$'\t' read -r key_name; do
|
|
3592
3840
|
[ -z "$key_name" ] && continue
|
|
@@ -5348,7 +5596,7 @@ Re-do the grouping. Every input hash MUST appear in EXACTLY ONE \"commits\" arra
|
|
|
5348
5596
|
local max_out=$(( commit_count * 120 ))
|
|
5349
5597
|
[ "$max_out" -lt 2048 ] && max_out=2048
|
|
5350
5598
|
[ "$max_out" -gt 16384 ] && max_out=16384
|
|
5351
|
-
call_ai_api "$system_prompt" "$user_prompt" "$max_out" "
|
|
5599
|
+
call_ai_api "$system_prompt" "$user_prompt" "$max_out" "" '{"type":"json_object"}' "low"
|
|
5352
5600
|
}
|
|
5353
5601
|
function squash_parse_ai_plan {
|
|
5354
5602
|
local response="$1"
|
|
@@ -6724,6 +6972,18 @@ function get_max_split_groups {
|
|
|
6724
6972
|
[ "$v" -gt 20 ] && v=20
|
|
6725
6973
|
printf '%s' "$v"
|
|
6726
6974
|
}
|
|
6975
|
+
function should_attempt_ai_grouping {
|
|
6976
|
+
local scope_count="$1" mode="$2"
|
|
6977
|
+
case "$mode" in
|
|
6978
|
+
always) return 0 ;;
|
|
6979
|
+
never) return 1 ;;
|
|
6980
|
+
esac
|
|
6981
|
+
case "$scope_count" in
|
|
6982
|
+
''|*[!0-9]*) return 1 ;;
|
|
6983
|
+
esac
|
|
6984
|
+
[ "$scope_count" -ge 3 ] && return 0
|
|
6985
|
+
return 1
|
|
6986
|
+
}
|
|
6727
6987
|
#
|
|
6728
6988
|
#
|
|
6729
6989
|
#
|
|
@@ -6870,7 +7130,7 @@ A feature is defined by WHAT the change accomplishes, read from the actual code
|
|
|
6870
7130
|
|
|
6871
7131
|
Input arrives in XML tags: a list of staged files, the actual diff, a diff summary, recent commit messages (for scope-naming conventions), and folder-derived candidate scope tokens.
|
|
6872
7132
|
|
|
6873
|
-
Task: assign EVERY staged file to exactly one feature group. Use 1 group only if all changes truly form a single feature; otherwise use 2 to ${max_groups} groups. Never exceed ${max_groups} groups — prefer a broader feature over several tiny ones. Name each group with a short lowercase conventional-commit scope describing the FEATURE, matching the style of <recent_commits>. <heuristic_candidates> are folder-derived hints — use one only if it happens to match a feature's purpose. Use \"misc\" only for unrelated incidental files with no feature (e.g., README, top-level config).
|
|
7133
|
+
Task: assign EVERY staged file to exactly one feature group. Use 1 group only if all changes truly form a single feature; otherwise use 2 to ${max_groups} groups. Never exceed ${max_groups} groups — prefer a broader feature over several tiny ones. Name each group with a short lowercase conventional-commit scope describing the FEATURE — the bare scope token only (e.g. \"auth\", never a full prefix like \"feat(auth)\" or a type like \"docs:\"), matching the scope-naming style of <recent_commits>. <heuristic_candidates> are folder-derived hints — use one only if it happens to match a feature's purpose. Use \"misc\" only for unrelated incidental files with no feature (e.g., README, top-level config).
|
|
6874
7134
|
|
|
6875
7135
|
Output format: TSV only. One line per staged file in the form <scope><TAB><file_path>. No header, no prose, no markdown fences, no surrounding quotes. Every file from <staged_files> MUST appear exactly once. The file paths must be byte-identical to those in <staged_files>."
|
|
6876
7136
|
|
|
@@ -6896,8 +7156,9 @@ ${heuristic_hint}
|
|
|
6896
7156
|
|
|
6897
7157
|
Read <diff> to decide which files implement the same feature. Output TSV (scope<TAB>file) for every staged file. No prose."
|
|
6898
7158
|
local ai_response
|
|
6899
|
-
ai_response=$(call_ai_api "$system_prompt" "$user_prompt" "$AI_MAX_TOKENS_FULL" "
|
|
7159
|
+
ai_response=$(call_ai_api "$system_prompt" "$user_prompt" "$AI_MAX_TOKENS_FULL" "" "" "low" 2>/dev/null)
|
|
6900
7160
|
if [ $? -ne 0 ] || [ -z "$ai_response" ]; then
|
|
7161
|
+
ai_grouping_fail_reason="API error"
|
|
6901
7162
|
return 1
|
|
6902
7163
|
fi
|
|
6903
7164
|
ai_response=$(printf '%s' "$ai_response" | LC_ALL=C sed -e 's/^```[a-zA-Z]*$//' -e 's/^```$//')
|
|
@@ -6905,6 +7166,14 @@ Read <diff> to decide which files implement the same feature. Output TSV (scope<
|
|
|
6905
7166
|
while IFS= read -r f; do
|
|
6906
7167
|
[ -n "$f" ] && gset_add staged_set "$f"
|
|
6907
7168
|
done <<< "$staged_files"
|
|
7169
|
+
gmap_clear folder_scope_of
|
|
7170
|
+
local _fk _ff
|
|
7171
|
+
for _fk in "${split_group_keys[@]}"; do
|
|
7172
|
+
while IFS= read -r _ff; do
|
|
7173
|
+
[ -n "$_ff" ] && gmap_set folder_scope_of "$_ff" "$_fk"
|
|
7174
|
+
done <<< "$(gmap_get split_groups "$_fk")"
|
|
7175
|
+
done
|
|
7176
|
+
gmap_clear assigned_set
|
|
6908
7177
|
gmap_clear new_groups
|
|
6909
7178
|
local -a new_keys=()
|
|
6910
7179
|
local line scope file tab_count
|
|
@@ -6916,11 +7185,18 @@ Read <diff> to decide which files implement the same feature. Output TSV (scope<
|
|
|
6916
7185
|
[ "$tab_count" -ne 1 ] && continue
|
|
6917
7186
|
scope="${line%% *}"
|
|
6918
7187
|
file="${line#* }"
|
|
7188
|
+
scope="${scope%:}"
|
|
7189
|
+
local type_scope_re='^[a-zA-Z0-9._/-]+\(([a-zA-Z0-9._/-]+)\)$'
|
|
7190
|
+
if [[ "$scope" =~ $type_scope_re ]]; then
|
|
7191
|
+
scope="${BASH_REMATCH[1]}"
|
|
7192
|
+
fi
|
|
6919
7193
|
if ! [[ "$scope" =~ ^[a-zA-Z0-9._/-]+$ ]]; then
|
|
6920
7194
|
continue
|
|
6921
7195
|
fi
|
|
6922
7196
|
scope=$(printf '%s' "$scope" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
|
6923
7197
|
gset_has staged_set "$file" || continue
|
|
7198
|
+
gset_has assigned_set "$file" && continue
|
|
7199
|
+
gset_add assigned_set "$file"
|
|
6924
7200
|
if ! gmap_has new_groups "$scope"; then
|
|
6925
7201
|
new_keys+=("$scope")
|
|
6926
7202
|
gmap_set new_groups "$scope" "$file"
|
|
@@ -6928,15 +7204,44 @@ Read <diff> to decide which files implement the same feature. Output TSV (scope<
|
|
|
6928
7204
|
gmap_set new_groups "$scope" "$(gmap_get new_groups "$scope")"$'\n'"$file"
|
|
6929
7205
|
fi
|
|
6930
7206
|
done <<< "$ai_response"
|
|
6931
|
-
local total_assigned=0 total_staged
|
|
7207
|
+
local total_assigned=0 total_staged unmatched
|
|
6932
7208
|
local key
|
|
6933
7209
|
for key in "${new_keys[@]}"; do
|
|
6934
7210
|
total_assigned=$((total_assigned + $(printf '%s\n' "$(gmap_get new_groups "$key")" | grep -c .)))
|
|
6935
7211
|
done
|
|
6936
7212
|
total_staged=$(printf '%s\n' "$staged_files" | grep -c .)
|
|
6937
|
-
if [
|
|
7213
|
+
if [ ${#new_keys[@]} -lt 1 ] || [ "$total_assigned" -eq 0 ]; then
|
|
7214
|
+
ai_grouping_fail_reason="no usable grouping returned"
|
|
7215
|
+
return 1
|
|
7216
|
+
fi
|
|
7217
|
+
unmatched=$((total_staged - total_assigned))
|
|
7218
|
+
[ "$unmatched" -lt 0 ] && unmatched=0 # dedup should prevent this; be safe
|
|
7219
|
+
if [ "$unmatched" -gt 0 ] && [ $((unmatched * 5)) -gt "$total_staged" ]; then
|
|
7220
|
+
ai_grouping_fail_reason="${unmatched} of ${total_staged} files unassigned"
|
|
6938
7221
|
return 1
|
|
6939
7222
|
fi
|
|
7223
|
+
if [ "$unmatched" -gt 0 ]; then
|
|
7224
|
+
local single_group=""
|
|
7225
|
+
[ ${#new_keys[@]} -eq 1 ] && single_group="${new_keys[0]}"
|
|
7226
|
+
local sf leftover_scope
|
|
7227
|
+
while IFS= read -r sf; do
|
|
7228
|
+
[ -z "$sf" ] && continue
|
|
7229
|
+
gset_has assigned_set "$sf" && continue
|
|
7230
|
+
if [ -n "$single_group" ]; then
|
|
7231
|
+
leftover_scope="$single_group"
|
|
7232
|
+
else
|
|
7233
|
+
leftover_scope=$(gmap_get folder_scope_of "$sf")
|
|
7234
|
+
[ -z "$leftover_scope" ] && leftover_scope="misc"
|
|
7235
|
+
fi
|
|
7236
|
+
if ! gmap_has new_groups "$leftover_scope"; then
|
|
7237
|
+
new_keys+=("$leftover_scope")
|
|
7238
|
+
gmap_set new_groups "$leftover_scope" "$sf"
|
|
7239
|
+
else
|
|
7240
|
+
gmap_set new_groups "$leftover_scope" "$(gmap_get new_groups "$leftover_scope")"$'\n'"$sf"
|
|
7241
|
+
fi
|
|
7242
|
+
gset_add assigned_set "$sf"
|
|
7243
|
+
done <<< "$staged_files"
|
|
7244
|
+
fi
|
|
6940
7245
|
gmap_clear split_groups
|
|
6941
7246
|
split_group_keys=()
|
|
6942
7247
|
for key in "${new_keys[@]}"; do
|
|
@@ -7353,18 +7658,17 @@ function try_offer_commit_split {
|
|
|
7353
7658
|
local ai_grouping
|
|
7354
7659
|
ai_grouping=$(get_config_value gitbasher.commit-ai-grouping "auto")
|
|
7355
7660
|
local should_use_ai="false"
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
*) should_use_ai="true" ;; # auto + always: attempt AI
|
|
7359
|
-
esac
|
|
7360
|
-
if [ -z "$llm" ]; then
|
|
7361
|
-
should_use_ai="false"
|
|
7661
|
+
if [ -n "$llm" ] && should_attempt_ai_grouping "${#split_group_keys[@]}" "$ai_grouping"; then
|
|
7662
|
+
should_use_ai="true"
|
|
7362
7663
|
fi
|
|
7363
7664
|
if [ "$should_use_ai" = "true" ] && check_ai_available 2>/dev/null; then
|
|
7364
7665
|
echo
|
|
7365
7666
|
echo -e "${YELLOW}Grouping changes by feature with AI...${ENDCOLOR}"
|
|
7667
|
+
local ai_grouping_fail_reason=""
|
|
7366
7668
|
if ! group_files_by_feature_with_ai; then
|
|
7367
|
-
|
|
7669
|
+
local reason_suffix=""
|
|
7670
|
+
[ -n "$ai_grouping_fail_reason" ] && reason_suffix=" (${ai_grouping_fail_reason})"
|
|
7671
|
+
echo -e "${YELLOW}AI feature grouping failed${reason_suffix}, using folder-based grouping.${ENDCOLOR}"
|
|
7368
7672
|
fi
|
|
7369
7673
|
consolidate_split_groups "$(get_max_split_groups)"
|
|
7370
7674
|
fi
|