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
+56 -1
View File
@@ -3,11 +3,13 @@ package workspace
import (
"os"
"path/filepath"
"strings"
"sync"
)
type WorkspaceInfo struct {
HasGit bool
GitBranch string
HasNodeProject bool
HasGoProject bool
HasRustProject bool
@@ -57,9 +59,55 @@ func Detect(cwd string) WorkspaceInfo {
}
}
info.HasGit, info.GitBranch = detectGitInfo(cwd)
return info
}
func resolveGitHeadPath(cwd string) (hasGit bool, headPath string) {
dir := cwd
for dir != "" {
gitPath := filepath.Join(dir, ".git")
info, err := os.Stat(gitPath)
if err == nil {
if info.IsDir() {
return true, filepath.Join(gitPath, "HEAD")
}
content, errRead := os.ReadFile(gitPath)
if errRead == nil {
s := strings.TrimSpace(string(content))
if after, ok := strings.CutPrefix(s, "gitdir: "); ok {
gitDir := strings.TrimSpace(after)
if !filepath.IsAbs(gitDir) {
gitDir = filepath.Join(dir, gitDir)
}
return true, filepath.Join(gitDir, "HEAD")
}
}
return true, "" // found .git but couldn't resolve HEAD
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return false, ""
}
func detectGitInfo(cwd string) (hasGit bool, branch string) {
hasGit, headPath := resolveGitHeadPath(cwd)
if headPath != "" {
if data, errHead := os.ReadFile(headPath); errHead == nil {
s := strings.TrimSpace(string(data))
if after, ok := strings.CutPrefix(s, "ref: refs/heads/"); ok {
return hasGit, after
}
}
}
return hasGit, ""
}
type cacheEntry struct {
key string // cwd + "|" + dirModTime
info WorkspaceInfo
@@ -71,7 +119,7 @@ var (
)
// DetectCached returns cached workspace info, invalidating when directory modtime changes
// this handles mid-session file creation (e.g. go mod init) without requiring cd
// or when the Git HEAD file changes (to catch branch switches that don't affect cwd modtime).
func DetectCached(cwd string) WorkspaceInfo {
dirInfo, err := os.Stat(cwd)
if err != nil {
@@ -79,6 +127,13 @@ func DetectCached(cwd string) WorkspaceInfo {
}
key := cwd + "|" + dirInfo.ModTime().String()
// Incorporate Git HEAD modtime into cache key to catch branch switches
if _, headPath := resolveGitHeadPath(cwd); headPath != "" {
if headInfo, err := os.Stat(headPath); err == nil {
key += "|HEAD:" + headInfo.ModTime().String()
}
}
wsCacheMu.Lock()
defer wsCacheMu.Unlock()