feat: implement file referencing with @filename autocomplete in chat input (#118)

This commit is contained in:
João Salavisa
2026-01-28 22:17:29 +00:00
committed by GitHub
parent 83e3429089
commit 7f0592cccd
15 changed files with 497 additions and 21 deletions
@@ -82,6 +82,7 @@ func (s *Server) setupRoutes() {
// Workspace file operations (simple HTTP)
api.GET("/workspace/tree", s.handleFileTree)
api.GET("/workspace/file/content", s.handleFileContent)
api.GET("/workspace/search", s.handleFileSearch)
// Shell access (HTTP endpoints only - streaming is via /workspace/stream)
api.GET("/shell/status", s.handleShellStatus)
@@ -151,6 +151,21 @@ func (s *Server) handleFileContent(c *gin.Context) {
c.JSON(200, types.FileContentResponse{Path: path, Content: content, Size: size})
}
// handleFileSearch handles file search requests via HTTP GET
func (s *Server) handleFileSearch(c *gin.Context) {
query := c.Query("q")
limit := 20
if l := c.Query("limit"); l != "" {
if parsed := mustParseInt(l); parsed > 0 {
limit = parsed
}
}
files := s.procMgr.GetWorkspaceTracker().SearchFiles(query, limit)
c.JSON(200, types.FileSearchResponse{Files: files})
}
// mustParseInt parses a string to int, returns 0 on error
func mustParseInt(s string) int {
var n int
@@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
@@ -1183,3 +1184,65 @@ func (wt *WorkspaceTracker) GetFileContent(reqPath string) (string, int64, error
return string(content), info.Size(), nil
}
// scoredMatch holds a file path and its match score for sorting
type scoredMatch struct {
path string
score int
}
// SearchFiles searches for files matching the query string.
// It uses fuzzy matching with scoring based on how well the query matches.
func (wt *WorkspaceTracker) SearchFiles(query string, limit int) []string {
if query == "" {
return []string{}
}
if limit <= 0 {
limit = 20
}
query = strings.ToLower(query)
var matches []scoredMatch
wt.mu.RLock()
files := wt.currentFiles.Files
wt.mu.RUnlock()
for _, file := range files {
path := file.Path
lowerPath := strings.ToLower(path)
name := filepath.Base(lowerPath)
score := 0
if name == query {
score = 100 // Exact filename match
} else if strings.HasPrefix(name, query) {
score = 75 // Filename starts with query
} else if strings.Contains(name, query) {
score = 50 // Filename contains query
} else if strings.Contains(lowerPath, query) {
score = 25 // Path contains query
}
if score > 0 {
matches = append(matches, scoredMatch{path: path, score: score})
}
}
// Sort by score descending
sort.Slice(matches, func(i, j int) bool {
if matches[i].score != matches[j].score {
return matches[i].score > matches[j].score
}
// Secondary sort by path length (shorter paths first)
return len(matches[i].path) < len(matches[j].path)
})
// Return top limit results
result := make([]string, 0, limit)
for i := 0; i < len(matches) && i < limit; i++ {
result = append(result, matches[i].path)
}
return result
}