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:
VERSE
2026-07-26 19:10:56 +07:00
committed by GitHub
parent 85ac76e85a
commit 6ffb9f83f2
20 changed files with 1004 additions and 72 deletions
+75 -33
View File
@@ -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--
}
}