| 🛟 Updated. 28d5985 k33g 21h ago | 1 | // The project's files as an agent window offers them: what "@" lists in the |
| 2 | // box, and what a mention is resolved against when a prompt is sent. |
| 3 | |
| 4 | package app |
| 5 | |
| 6 | import ( |
| 7 | "io/fs" |
| 8 | "path/filepath" |
| 9 | |
| 📦 Turbo Core f3ade8d k33g 13h ago | 10 | "rickub.com/turbo-editors/turbo-core/acp" |
| 🛟 Updated. 28d5985 k33g 21h ago | 11 | ) |
| 12 | |
| 13 | // projectFileLimit caps how many files an agent window is offered. |
| 14 | // |
| 15 | // Past this the picker is a list to scroll rather than a list to choose from, |
| 16 | // and a walk of a monorepo on every "@" would make the box stutter. Typing one |
| 17 | // more letter after the "@" is the answer for a project this size, exactly as |
| 18 | // it is for the Open File dialog. |
| 19 | const projectFileLimit = 5000 |
| 20 | |
| 21 | // projectFiles lists the project for the agent windows. |
| 22 | // |
| 23 | // It walks on each call rather than caching: the picker asks once when "@" is |
| 24 | // typed and keeps the answer while it is open, and a list that never noticed a |
| 25 | // file the agent just created would be wrong at the moment it mattered. |
| 26 | func (a *App) projectFiles() []acp.Mention { |
| 27 | return listProjectFiles(a.projectRoot(), projectFileLimit) |
| 28 | } |
| 29 | |
| 30 | // listProjectFiles walks root and returns its regular files, relative to root |
| 31 | // with forward slashes, in the order the walk finds them. The .git directory |
| 32 | // is skipped for the reason the file tree skips it: nothing in it is meant to |
| 33 | // be read by hand, and it is large enough to bury everything else. The walk |
| 34 | // stops at limit files. |
| 35 | // |
| 36 | // listProjectFiles("/src/p", 5000) // [{app/menus.go /src/p/app/menus.go} …] |
| 37 | func listProjectFiles(root string, limit int) []acp.Mention { |
| 38 | var out []acp.Mention |
| 39 | _ = filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { |
| 40 | if err != nil || entry.IsDir() { |
| 41 | return skipGit(entry, err) |
| 42 | } |
| 43 | if entry.Type().IsRegular() { |
| 44 | out = appendFile(out, root, path) |
| 45 | } |
| 46 | if len(out) >= limit { |
| 47 | return filepath.SkipAll |
| 48 | } |
| 49 | return nil |
| 50 | }) |
| 51 | return out |
| 52 | } |
| 53 | |
| 54 | // skipGit tells the walk to leave .git alone, and to carry on past a directory |
| 55 | // it could not read rather than stop the whole listing for it. |
| 56 | func skipGit(entry fs.DirEntry, err error) error { |
| 57 | if err == nil && entry.Name() == ".git" { |
| 58 | return filepath.SkipDir |
| 59 | } |
| 60 | return nil |
| 61 | } |
| 62 | |
| 63 | // appendFile adds one file to the list, named relative to the project with |
| 64 | // forward slashes whatever the platform, because that is how it is typed. |
| 65 | func appendFile(files []acp.Mention, root, path string) []acp.Mention { |
| 66 | relative, err := filepath.Rel(root, path) |
| 67 | if err != nil { |
| 68 | return files |
| 69 | } |
| 70 | return append(files, acp.Mention{Name: filepath.ToSlash(relative), Path: path}) |
| 71 | } |