feat(scoring): transition workflow learning and fix history UX (#39)
- add transition scoring engine to learn sequential workflows per directory using command skeletons - prioritize current active git branch over older branches in suggestions - restore strict chronological order for history navigation and bypass AI re-ranking - add in-memory session history for instant access to just-run commands - fix PTY prompt not syncing when using arrow keys in the history menu - fix history tie-breaker to properly prioritize newer commands by assigning larger IDs - fix potential mutex deadlock between bufferMu and pty write during history navigation - fix workspace git detection to remove depth limits and correctly identify repositories with detached HEADs - fix SQLite connection leaks in frecency queries - fix global state leaks in history tests by snapshotting and restoring registry states - modernize string prefix checks using strings.CutPrefix
This commit is contained in:
+75
-33
@@ -13,6 +13,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
sessionHistory []string
|
||||
sessionHistoryMu sync.Mutex
|
||||
|
||||
historyCache []string
|
||||
idMapCache map[string]int
|
||||
searcherCache *fuzzy.Searcher
|
||||
@@ -20,6 +23,24 @@ var (
|
||||
lastModTime int64
|
||||
)
|
||||
|
||||
func RecordSessionCommand(cmd string) {
|
||||
cmd = strings.TrimSpace(cmd)
|
||||
if cmd == "" {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
sessionHistoryMu.Lock()
|
||||
defer sessionHistoryMu.Unlock()
|
||||
|
||||
if len(sessionHistory) > 0 && sessionHistory[len(sessionHistory)-1] == cmd {
|
||||
return
|
||||
}
|
||||
sessionHistory = append(sessionHistory, cmd)
|
||||
historyCache = nil // invalidate to merge session history on next search
|
||||
}
|
||||
|
||||
type HistResult struct {
|
||||
ID int
|
||||
Cmd string
|
||||
@@ -65,61 +86,82 @@ func SearchHistory(query string, aliases map[string]string) ([]HistResult, error
|
||||
// lazy load history if cache is empty
|
||||
if len(historyCache) == 0 {
|
||||
file, err := os.Open(histFile)
|
||||
if err != nil {
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
if file != nil {
|
||||
defer func() { _ = file.Close() }()
|
||||
}
|
||||
|
||||
var allCmds []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
cmd := line
|
||||
if file != nil {
|
||||
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 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
|
||||
}
|
||||
}
|
||||
if isTimestamp {
|
||||
} else if shellName == "fish" {
|
||||
if after, ok := strings.CutPrefix(line, "- cmd: "); ok {
|
||||
cmd = after
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
cmd = strings.TrimSpace(cmd)
|
||||
if cmd != "" {
|
||||
allCmds = append(allCmds, cmd)
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// build historyCache backwards so newest commands come first
|
||||
seen := make(map[string]bool)
|
||||
historyCache = nil
|
||||
idMapCache = make(map[string]int)
|
||||
|
||||
currentID := len(sessionHistory) + len(allCmds)
|
||||
|
||||
sessionHistoryMu.Lock()
|
||||
for i := len(sessionHistory) - 1; i >= 0; i-- {
|
||||
cmd := sessionHistory[i]
|
||||
if !seen[cmd] {
|
||||
historyCache = append(historyCache, cmd)
|
||||
seen[cmd] = true
|
||||
idMapCache[cmd] = currentID
|
||||
currentID--
|
||||
}
|
||||
}
|
||||
sessionHistoryMu.Unlock()
|
||||
|
||||
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
|
||||
idMapCache[cmd] = currentID
|
||||
currentID--
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user