// The project's files as an agent window offers them: what "@" lists in the // box, and what a mention is resolved against when a prompt is sent. package app import ( "io/fs" "path/filepath" "rickub.com/turbo-editors/turbo-core/acp" ) // projectFileLimit caps how many files an agent window is offered. // // Past this the picker is a list to scroll rather than a list to choose from, // and a walk of a monorepo on every "@" would make the box stutter. Typing one // more letter after the "@" is the answer for a project this size, exactly as // it is for the Open File dialog. const projectFileLimit = 5000 // projectFiles lists the project for the agent windows. // // It walks on each call rather than caching: the picker asks once when "@" is // typed and keeps the answer while it is open, and a list that never noticed a // file the agent just created would be wrong at the moment it mattered. func (a *App) projectFiles() []acp.Mention { return listProjectFiles(a.projectRoot(), projectFileLimit) } // listProjectFiles walks root and returns its regular files, relative to root // with forward slashes, in the order the walk finds them. The .git directory // is skipped for the reason the file tree skips it: nothing in it is meant to // be read by hand, and it is large enough to bury everything else. The walk // stops at limit files. // // listProjectFiles("/src/p", 5000) // [{app/menus.go /src/p/app/menus.go} …] func listProjectFiles(root string, limit int) []acp.Mention { var out []acp.Mention _ = filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { if err != nil || entry.IsDir() { return skipGit(entry, err) } if entry.Type().IsRegular() { out = appendFile(out, root, path) } if len(out) >= limit { return filepath.SkipAll } return nil }) return out } // skipGit tells the walk to leave .git alone, and to carry on past a directory // it could not read rather than stop the whole listing for it. func skipGit(entry fs.DirEntry, err error) error { if err == nil && entry.Name() == ".git" { return filepath.SkipDir } return nil } // appendFile adds one file to the list, named relative to the project with // forward slashes whatever the platform, because that is how it is typed. func appendFile(files []acp.Mention, root, path string) []acp.Mention { relative, err := filepath.Rel(root, path) if err != nil { return files } return append(files, acp.Mention{Name: filepath.ToSlash(relative), Path: path}) }