| /** | |
| * Utilities for ranking workspace files by "importance" so that the file-tab | |
| * top-row surfaces the entry points (`index.html`, `README.md`, `package.json`, | |
| * etc.) ahead of nested utility modules. | |
| */ | |
| /** | |
| * File basenames (lowercased) that are almost always the entrypoint for a | |
| * project. Lower index = higher priority. | |
| */ | |
| const HIGH_PRIORITY_BASENAMES: string[] = [ | |
| "index.html", | |
| "index.htm", | |
| "readme.md", | |
| "readme", | |
| "main.html", | |
| "app.html", | |
| "index.js", | |
| "index.ts", | |
| "index.tsx", | |
| "index.jsx", | |
| "main.py", | |
| "app.py", | |
| "main.go", | |
| "main.rs", | |
| "main.java", | |
| "main.c", | |
| "main.cpp", | |
| "package.json", | |
| "pyproject.toml", | |
| "cargo.toml", | |
| "go.mod", | |
| "pom.xml", | |
| "dockerfile", | |
| "makefile", | |
| ]; | |
| /** | |
| * Filenames that are useful but typically of secondary interest compared to | |
| * the entrypoints above. | |
| */ | |
| const SECONDARY_BASENAMES: string[] = [ | |
| "license", | |
| "license.md", | |
| "license.txt", | |
| "changelog.md", | |
| "agents.md", | |
| "tsconfig.json", | |
| ".env.sample", | |
| ".env.example", | |
| ]; | |
| function getBasename(path: string): string { | |
| const idx = path.lastIndexOf("/"); | |
| return (idx === -1 ? path : path.slice(idx + 1)).toLowerCase(); | |
| } | |
| function pathDepth(path: string): number { | |
| // Filter empty segments so leading/trailing/double slashes don't inflate | |
| // depth (e.g. `/src/index.html`, `src//index.html`, `src/` should all | |
| // count the same as `src/index.html`). Matches the convention already | |
| // used by `buildFileTree` in `file-tree.ts`. | |
| return path.split("/").filter(Boolean).length - 1; | |
| } | |
| /** | |
| * Rank a path *within its own depth bucket*: high-priority entrypoints | |
| * first (in the order listed), then secondary supporting files, then | |
| * everything else. Depth is the primary sort axis applied by | |
| * {@link sortFilesByPriority}; this score is the tie-breaker for paths at | |
| * the same depth. | |
| */ | |
| export function filePriorityScore(path: string): number { | |
| const base = getBasename(path); | |
| const highIdx = HIGH_PRIORITY_BASENAMES.indexOf(base); | |
| if (highIdx !== -1) return highIdx; | |
| const secondaryIdx = SECONDARY_BASENAMES.indexOf(base); | |
| if (secondaryIdx !== -1) return 1000 + secondaryIdx; | |
| return 10000; | |
| } | |
| /** | |
| * Returns a copy of `paths` sorted so the most likely "landing files" come | |
| * first. | |
| * | |
| * Sort order: | |
| * 1. Shallower paths beat deeper ones, unconditionally. A top-level | |
| * `README.md` outranks `foo/bar/index.html` even though `index.html` | |
| * is a more "important" basename — the user almost always cares more | |
| * about top-level files when they first open a project. | |
| * 2. Within the same depth, basenames are ordered by importance | |
| * (`index.html` before `README.md` before random utility modules). | |
| * 3. Final tie-breaker is alphabetical. | |
| */ | |
| export function sortFilesByPriority(paths: string[]): string[] { | |
| return [...paths].sort((a, b) => { | |
| const depthDiff = pathDepth(a) - pathDepth(b); | |
| if (depthDiff !== 0) return depthDiff; | |
| const scoreDiff = filePriorityScore(a) - filePriorityScore(b); | |
| if (scoreDiff !== 0) return scoreDiff; | |
| return a.localeCompare(b); | |
| }); | |
| } | |