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
+30 -1
View File
@@ -12,6 +12,7 @@ type ScoreBreakdown struct {
BasePriority int
ContextBonus int
Frecency int
Transition int
MatchQuality int
}
@@ -25,13 +26,15 @@ type ScoreConfig struct {
WeightBasePriority float64
WeightContextBonus float64
WeightFrecency float64
WeightTransition float64
WeightMatchQuality float64
}
var DefaultScoreConfig = ScoreConfig{
WeightBasePriority: 0.30,
WeightContextBonus: 0.25,
WeightFrecency: 0.25,
WeightFrecency: 0.15,
WeightTransition: 0.10,
WeightMatchQuality: 0.20,
}
@@ -71,11 +74,13 @@ func ScoreWithConfig(suggestions []spec.Suggestion, signals SignalSet, config Sc
bp := basePriorityFor(s)
cb := ApplyContextRules(signals.Workspace, s.Cmd)
frec := normFrec[i]
trans := transitionScoreFor(ExtractSkeleton(s.Cmd), signals.TransitionEntries, signals.TransitionIsLocal)
mq := matchQualityScore(s.Cmd, signals.Query)
total := config.WeightBasePriority*float64(bp) +
config.WeightContextBonus*float64(cb) +
config.WeightFrecency*float64(frec) +
config.WeightTransition*float64(trans) +
config.WeightMatchQuality*float64(mq)
scored[i] = ScoredSuggestion{
@@ -85,6 +90,7 @@ func ScoreWithConfig(suggestions []spec.Suggestion, signals SignalSet, config Sc
BasePriority: bp,
ContextBonus: cb,
Frecency: frec,
Transition: trans,
MatchQuality: mq,
},
}
@@ -94,6 +100,9 @@ func ScoreWithConfig(suggestions []spec.Suggestion, signals SignalSet, config Sc
if scored[i].Score != scored[j].Score {
return scored[i].Score > scored[j].Score
}
if scored[i].Breakdown.Transition != scored[j].Breakdown.Transition {
return scored[i].Breakdown.Transition > scored[j].Breakdown.Transition
}
if scored[i].Breakdown.Frecency != scored[j].Breakdown.Frecency {
return scored[i].Breakdown.Frecency > scored[j].Breakdown.Frecency
}
@@ -106,6 +115,26 @@ func ScoreWithConfig(suggestions []spec.Suggestion, signals SignalSet, config Sc
return scored
}
func transitionScoreFor(cmdSkeleton string, entries []TransitionEntry, isLocal bool) int {
if len(entries) == 0 {
return 0 // cold-start: no data, contributes 0 (must check before accessing entries[0])
}
maxCount := entries[0].Count
if maxCount <= 0 {
return 0
}
for _, e := range entries {
if e.NextSkeleton == cmdSkeleton {
score := (float64(e.Count) / float64(maxCount)) * 100.0
if !isLocal {
score *= 0.7
}
return int(math.Round(score))
}
}
return 0
}
func basePriorityFor(s spec.Suggestion) int {
if s.Priority > 0 {
if s.Priority > 100 {