Enhance lookup mechanics and priority propagation (`spec/lookup.go`) Remove the requirement of typing a leading `-` when searching for options, allowing flag suggestions to surface naturally during input Accurately forward the `Priority` field from registry specs into the `Suggestion` struct so the scoring engine evaluates relevance properly Integrate scoring into the main suggestion pipeline (`root/suggestions.go`) Collect context signals using `scoring.CollectSignals(cwd, query, rootCmd, store)` with the live working directory retrieved from `spec.GetCWD()` Pass the entire deduplicated list across spec, history and AI items through `scoring.Score(deduped, signals)` to compute scores and sort by descending order before rendering on screen Record execution history in Frecency Store (`root/wrapper.go`) Save the last executed command to `lastSubmittedCommand` whenever Enter is pressed or a suggestion is selected Trigger `store.Record(...)` asynchronously within the `IRIS_CMD_STOP` hook using a dedicated goroutine guarded by panic recovery and timeout limits, preventing blocks on the main shell thread Optimize workspace identification for AI (`internal/ai/context_provider.go`) Use `workspace.DetectCached(cwd)` to instantly fetch ecosystem context across Git, Node, Go, Rust, Python and Docker alongside active git branch state This caching mechanism avoids redundant subprocess execution when outside git repositories, significantly speeding up prompt context generation
261 lines
5.4 KiB
Go
261 lines
5.4 KiB
Go
package integration
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/versenilvis/fuzzy"
|
|
"github.com/versenilvis/iris/integration/shell"
|
|
)
|
|
|
|
var (
|
|
historyCache []string
|
|
idMapCache map[string]int
|
|
searcherCache *fuzzy.Searcher
|
|
mu sync.Mutex
|
|
lastModTime int64
|
|
)
|
|
|
|
type HistResult struct {
|
|
ID int
|
|
Cmd string
|
|
FuzzyScore int
|
|
}
|
|
|
|
func init() {
|
|
idMapCache = make(map[string]int)
|
|
}
|
|
|
|
func SearchHistory(query string, aliases map[string]string) ([]HistResult, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
shellName := "bash"
|
|
if shell.Current != nil {
|
|
shellName = shell.Current.GetName()
|
|
}
|
|
|
|
var histFile string
|
|
switch shellName {
|
|
case "zsh":
|
|
histFile = filepath.Join(home, ".zsh_history")
|
|
case "fish":
|
|
histFile = filepath.Join(home, ".local", "share", "fish", "fish_history")
|
|
default:
|
|
histFile = filepath.Join(home, ".bash_history")
|
|
}
|
|
|
|
if info, err := os.Stat(histFile); err == nil {
|
|
if info.ModTime().UnixNano() > lastModTime {
|
|
historyCache = nil // force reload
|
|
idMapCache = make(map[string]int)
|
|
lastModTime = info.ModTime().UnixNano()
|
|
}
|
|
}
|
|
|
|
// lazy load history if cache is empty
|
|
if len(historyCache) == 0 {
|
|
file, err := os.Open(histFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
var allCmds []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
cmd := line
|
|
|
|
if shellName == "zsh" {
|
|
parts := strings.SplitN(line, ";", 2)
|
|
if len(parts) == 2 {
|
|
cmd = parts[1]
|
|
}
|
|
} else if shellName == "bash" {
|
|
if strings.HasPrefix(line, "#") && len(line) > 1 {
|
|
isTimestamp := true
|
|
for _, c := range line[1:] {
|
|
if c < '0' || c > '9' {
|
|
isTimestamp = false
|
|
break
|
|
}
|
|
}
|
|
if isTimestamp {
|
|
continue
|
|
}
|
|
}
|
|
} else if shellName == "fish" {
|
|
if after, ok := strings.CutPrefix(line, "- cmd: "); ok {
|
|
cmd = after
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
cmd = strings.TrimSpace(cmd)
|
|
if cmd != "" {
|
|
allCmds = append(allCmds, cmd)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// build historyCache backwards so newest commands come first
|
|
seen := make(map[string]bool)
|
|
for i := len(allCmds) - 1; i >= 0; i-- {
|
|
cmd := allCmds[i]
|
|
if !seen[cmd] {
|
|
historyCache = append(historyCache, cmd)
|
|
seen[cmd] = true
|
|
// we assign the ID as the original line number (1-indexed based on allCmds length)
|
|
idMapCache[cmd] = i + 1
|
|
}
|
|
}
|
|
|
|
searcherCache = fuzzy.NewPlainSearcher(historyCache)
|
|
}
|
|
|
|
if query == "" {
|
|
var results []HistResult
|
|
limit := min(len(historyCache), 100)
|
|
|
|
for i := range limit {
|
|
cmd := historyCache[i]
|
|
results = append(results, HistResult{
|
|
ID: idMapCache[cmd],
|
|
Cmd: cmd,
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
var alternativeQueries []string
|
|
for name, target := range aliases {
|
|
if target != "" {
|
|
qLow := strings.ToLower(query)
|
|
tLow := strings.ToLower(target)
|
|
nLow := strings.ToLower(name)
|
|
|
|
if qLow == tLow {
|
|
alternativeQueries = append(alternativeQueries, name)
|
|
} else if strings.HasPrefix(qLow, tLow+" ") {
|
|
suffix := query[len(target):]
|
|
alternativeQueries = append(alternativeQueries, name+suffix)
|
|
}
|
|
|
|
if qLow == nLow {
|
|
alternativeQueries = append(alternativeQueries, target)
|
|
} else if strings.HasPrefix(qLow, nLow+" ") {
|
|
suffix := query[len(name):]
|
|
alternativeQueries = append(alternativeQueries, target+suffix)
|
|
}
|
|
}
|
|
}
|
|
|
|
var results []HistResult
|
|
seenCmds := make(map[string]bool)
|
|
|
|
addMatches := func(q string) {
|
|
qLow := strings.ToLower(q)
|
|
queryFirstWord := ""
|
|
if strings.IndexByte(qLow, ' ') != -1 {
|
|
if fields := strings.Fields(qLow); len(fields) > 0 {
|
|
queryFirstWord = fields[0]
|
|
}
|
|
}
|
|
|
|
matches := searcherCache.SearchWithScores(q, &fuzzy.SearchOptions{Limit: 200})
|
|
for _, m := range matches {
|
|
if seenCmds[m.Str] {
|
|
continue
|
|
}
|
|
|
|
// filter results by command name match
|
|
firstWord := m.Str
|
|
if idx := strings.IndexByte(m.Str, ' '); idx != -1 {
|
|
firstWord = m.Str[:idx]
|
|
}
|
|
firstWordLow := strings.ToLower(firstWord)
|
|
|
|
if queryFirstWord != "" {
|
|
if firstWordLow != queryFirstWord {
|
|
continue
|
|
}
|
|
} else {
|
|
if !strings.HasPrefix(firstWordLow, qLow) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
seenCmds[m.Str] = true
|
|
results = append(results, HistResult{
|
|
ID: idMapCache[m.Str],
|
|
Cmd: m.Str,
|
|
FuzzyScore: m.Score,
|
|
})
|
|
}
|
|
}
|
|
|
|
addMatches(query)
|
|
|
|
for _, altQ := range alternativeQueries {
|
|
addMatches(altQ)
|
|
}
|
|
|
|
getTier := func(cmd, q string) int {
|
|
bestTier := 4
|
|
check := func(ql string) {
|
|
cmdLow := strings.ToLower(cmd)
|
|
qlLow := strings.ToLower(ql)
|
|
tier := 4
|
|
if cmdLow == qlLow {
|
|
tier = 1
|
|
} else if strings.HasPrefix(cmdLow, qlLow) {
|
|
tier = 2
|
|
} else if strings.Contains(cmdLow, qlLow) {
|
|
tier = 3
|
|
}
|
|
if tier < bestTier {
|
|
bestTier = tier
|
|
}
|
|
}
|
|
check(q)
|
|
for _, altQ := range alternativeQueries {
|
|
check(altQ)
|
|
}
|
|
return bestTier
|
|
}
|
|
|
|
tiers := make([]int, len(results))
|
|
for i, r := range results {
|
|
tiers[i] = getTier(r.Cmd, query)
|
|
}
|
|
|
|
sort.SliceStable(results, func(i, j int) bool {
|
|
tI := tiers[i]
|
|
tJ := tiers[j]
|
|
if tI != tJ {
|
|
return tI < tJ
|
|
}
|
|
|
|
if tI == 4 && results[i].FuzzyScore != results[j].FuzzyScore {
|
|
return results[i].FuzzyScore > results[j].FuzzyScore
|
|
}
|
|
|
|
return results[i].ID > results[j].ID
|
|
})
|
|
|
|
return results, nil
|
|
}
|