Files
iris-context/root/suggestions.go
T
VERSE 860b475b53 feat: scoring and frecency (#37)
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
2026-07-13 17:56:29 +07:00

153 lines
3.7 KiB
Go

package root
import (
"context"
"strings"
"sync"
"time"
"github.com/versenilvis/iris/integration"
"github.com/versenilvis/iris/internal/ai"
"github.com/versenilvis/iris/internal/config"
"github.com/versenilvis/iris/internal/logger"
"github.com/versenilvis/iris/internal/scoring"
"github.com/versenilvis/iris/spec"
)
// MergeResults collects and dedupes suggestions for a query and mode
func MergeResults(query string, mode string) []spec.Suggestion {
maxSugg := config.Get().UI.MaxSuggestions
seen := make(map[string]bool)
deduped := []spec.Suggestion{}
// always call lookup to scan aliases and get spec suggestions
logger.Debugf("Merge Calling Lookup for '%s'", query)
cmdResults := spec.Lookup(query)
// search history if in history mode
var histResults []integration.HistResult
if mode == "history" {
aliases := spec.GetAliasesCopy()
histResults, _ = integration.SearchHistory(query, aliases)
}
normalizedQuery := strings.TrimSpace(query)
// add suggestion helper to deduplicate
addSuggestion := func(s spec.Suggestion) {
normalizedCmd := strings.TrimSpace(s.Cmd)
if normalizedCmd == "" {
return
}
// filter exact match to avoid loops and redundant suggestions
if normalizedCmd == normalizedQuery {
return
}
if s.Source == "" {
s.Source = "spec"
if s.Confidence == 0 {
s.Confidence = 50
}
}
if !seen[s.Cmd] {
seen[s.Cmd] = true
deduped = append(deduped, s)
}
}
if mode == "history" {
// history mode: history first, then spec/alias
for _, h := range histResults {
addSuggestion(spec.Suggestion{
Cmd: h.Cmd,
Desc: "history",
Icon: "history",
Source: "history",
Confidence: 70,
})
}
for _, s := range cmdResults {
addSuggestion(s)
}
} else {
// spec mode: spec/alias only
for _, s := range cmdResults {
addSuggestion(s)
}
}
if aiSugg := GetCurrentAISuggestion(); aiSugg != nil {
normalizedCmd := strings.TrimSpace(aiSugg.Cmd)
if normalizedCmd != "" && normalizedCmd != normalizedQuery && strings.HasPrefix(strings.ToLower(normalizedCmd), strings.ToLower(normalizedQuery)) {
if !seen[aiSugg.Cmd] {
seen[aiSugg.Cmd] = true
deduped = append(deduped, *aiSugg)
} else {
for i, item := range deduped {
if item.Cmd == aiSugg.Cmd && aiSugg.Confidence > item.Confidence {
deduped[i].Confidence = aiSugg.Confidence
if deduped[i].Source == "" || deduped[i].Source == "spec" || deduped[i].Source == "history" {
deduped[i].Source = "ai"
}
break
}
}
}
}
}
cwd := spec.GetCWD()
tokens := spec.Tokenize(query)
rootCmd := ""
if len(tokens) > 0 {
rootCmd = tokens[0]
}
ctxTimeout, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
store, _ := scoring.GetFrecencyStore()
signals := scoring.CollectSignals(ctxTimeout, cwd, query, rootCmd, store)
scored := scoring.Score(deduped, signals)
finalResults := make([]spec.Suggestion, 0, len(scored))
for _, sc := range scored {
finalResults = append(finalResults, sc.Suggestion)
}
if len(finalResults) > maxSugg {
finalResults = finalResults[:maxSugg]
}
return finalResults
}
var (
aiEngine *ai.AIEngine
aiEngineOnce sync.Once
)
func GetAIEngine() *ai.AIEngine {
aiEngineOnce.Do(func() {
aiEngine = ai.NewAIEngine(nil)
for _, p := range ai.DefaultProviders {
aiEngine.RegisterProvider(p)
}
})
return aiEngine
}
var (
currentAISugg *spec.Suggestion
aiSuggMu sync.RWMutex
)
func SetCurrentAISuggestion(sugg *spec.Suggestion) {
aiSuggMu.Lock()
defer aiSuggMu.Unlock()
currentAISugg = sugg
}
func GetCurrentAISuggestion() *spec.Suggestion {
aiSuggMu.RLock()
defer aiSuggMu.RUnlock()
return currentAISugg
}