| author | |
| committer | |
| log | eae770b125553275e1ccfe4aebd88df28e818aed |
| tree | b4da16085d2afe2872ade096be06138a63ab037e |
| parent | 6b87ce3a0bd925edcbb26fbc6870a6ca81dead0b |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 0 insertions(+), 171 deletions(-)
apps/web/src/components/chat/changedFilesPresentation.test.ts deleted-69| ... | ... | @@ -1,69 +0,0 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { | |
| 4 | changedFileName, | |
| 5 | selectChangedFilePreview, | |
| 6 | shouldAutoExpandChangedFiles, | |
| 7 | summarizeChangedFileScopes, | |
| 8 | } from "./changedFilesPresentation"; | |
| 9 | ||
| 10 | describe("changed-files presentation", () => { | |
| 11 | it("auto-expands only small, low-churn latest changes", () => { | |
| 12 | const smallFiles = [ | |
| 13 | { path: "src/a.ts", kind: "modified", additions: 80, deletions: 20 }, | |
| 14 | { path: "src/b.ts", kind: "modified", additions: 60, deletions: 20 }, | |
| 15 | ]; | |
| 16 | ||
| 17 | expect(shouldAutoExpandChangedFiles(smallFiles, true)).toBe(true); | |
| 18 | expect(shouldAutoExpandChangedFiles(smallFiles, false)).toBe(false); | |
| 19 | expect( | |
| 20 | shouldAutoExpandChangedFiles( | |
| 21 | [{ path: "src/a.ts", kind: "modified", additions: 201, deletions: 0 }], | |
| 22 | true, | |
| 23 | ), | |
| 24 | ).toBe(false); | |
| 25 | expect( | |
| 26 | shouldAutoExpandChangedFiles( | |
| 27 | Array.from({ length: 6 }, (_, index) => ({ | |
| 28 | path: `src/${index}.ts`, | |
| 29 | kind: "modified", | |
| 30 | additions: 1, | |
| 31 | deletions: 0, | |
| 32 | })), | |
| 33 | true, | |
| 34 | ), | |
| 35 | ).toBe(false); | |
| 36 | }); | |
| 37 | ||
| 38 | it("summarizes the most prominent top-level scopes", () => { | |
| 39 | const files = [ | |
| 40 | { path: "apps/web/src/App.tsx", kind: "modified", additions: 1, deletions: 0 }, | |
| 41 | { path: "README.md", kind: "modified", additions: 1, deletions: 0 }, | |
| 42 | { path: "apps/server/src/index.ts", kind: "modified", additions: 1, deletions: 0 }, | |
| 43 | { path: "packages/shared/src/git.ts", kind: "modified", additions: 1, deletions: 0 }, | |
| 44 | { path: "apps\\mobile\\App.tsx", kind: "modified", additions: 1, deletions: 0 }, | |
| 45 | ]; | |
| 46 | ||
| 47 | expect(summarizeChangedFileScopes(files)).toEqual([ | |
| 48 | { label: "apps", fileCount: 3 }, | |
| 49 | { label: "root", fileCount: 1 }, | |
| 50 | { label: "packages", fileCount: 1 }, | |
| 51 | ]); | |
| 52 | }); | |
| 53 | ||
| 54 | it("previews files across different scopes before filling from one scope", () => { | |
| 55 | const files = [ | |
| 56 | { path: "apps/web/src/App.tsx", kind: "modified", additions: 1, deletions: 0 }, | |
| 57 | { path: "apps/web/src/App.test.tsx", kind: "modified", additions: 1, deletions: 0 }, | |
| 58 | { path: "packages/shared/src/git.ts", kind: "modified", additions: 1, deletions: 0 }, | |
| 59 | { path: "README.md", kind: "modified", additions: 1, deletions: 0 }, | |
| 60 | ]; | |
| 61 | ||
| 62 | expect(selectChangedFilePreview(files).map((file) => file.path)).toEqual([ | |
| 63 | "apps/web/src/App.tsx", | |
| 64 | "packages/shared/src/git.ts", | |
| 65 | "README.md", | |
| 66 | ]); | |
| 67 | expect(changedFileName("apps\\web\\src\\App.tsx")).toBe("App.tsx"); | |
| 68 | }); | |
| 69 | }); |
apps/web/src/components/chat/changedFilesPresentation.ts deleted-102| ... | ... | @@ -1,102 +0,0 @@ |
| 1 | import { type TurnDiffFileChange } from "../../types"; | |
| 2 | import { summarizeTurnDiffStats } from "../../lib/turnDiffTree"; | |
| 3 | ||
| 4 | export const CHANGED_FILES_AUTO_EXPAND_FILE_LIMIT = 5; | |
| 5 | export const CHANGED_FILES_AUTO_EXPAND_LINE_LIMIT = 200; | |
| 6 | export const CHANGED_FILES_PREVIEW_FILE_LIMIT = 3; | |
| 7 | export const CHANGED_FILES_PREVIEW_SCOPE_LIMIT = 4; | |
| 8 | ||
| 9 | export interface ChangedFilesScopeSummary { | |
| 10 | readonly label: string; | |
| 11 | readonly fileCount: number; | |
| 12 | } | |
| 13 | ||
| 14 | function pathSegments(pathValue: string): string[] { | |
| 15 | return pathValue | |
| 16 | .replaceAll("\\", "/") | |
| 17 | .split("/") | |
| 18 | .filter((segment) => segment.length > 0); | |
| 19 | } | |
| 20 | ||
| 21 | export function changedFileName(pathValue: string): string { | |
| 22 | return pathSegments(pathValue).at(-1) ?? pathValue; | |
| 23 | } | |
| 24 | ||
| 25 | function changedFileScope(pathValue: string): string { | |
| 26 | const segments = pathSegments(pathValue); | |
| 27 | return segments.length > 1 ? (segments[0] ?? "root") : "root"; | |
| 28 | } | |
| 29 | ||
| 30 | export function shouldAutoExpandChangedFiles( | |
| 31 | files: ReadonlyArray<TurnDiffFileChange>, | |
| 32 | isLatestTurn: boolean, | |
| 33 | ): boolean { | |
| 34 | if (!isLatestTurn || files.length > CHANGED_FILES_AUTO_EXPAND_FILE_LIMIT) { | |
| 35 | return false; | |
| 36 | } | |
| 37 | const stat = summarizeTurnDiffStats(files); | |
| 38 | return stat.additions + stat.deletions <= CHANGED_FILES_AUTO_EXPAND_LINE_LIMIT; | |
| 39 | } | |
| 40 | ||
| 41 | export function summarizeChangedFileScopes( | |
| 42 | files: ReadonlyArray<TurnDiffFileChange>, | |
| 43 | limit = CHANGED_FILES_PREVIEW_SCOPE_LIMIT, | |
| 44 | ): ChangedFilesScopeSummary[] { | |
| 45 | const scopes = new Map<string, { fileCount: number; firstIndex: number }>(); | |
| 46 | files.forEach((file, index) => { | |
| 47 | const label = changedFileScope(file.path); | |
| 48 | const current = scopes.get(label); | |
| 49 | scopes.set(label, { | |
| 50 | fileCount: (current?.fileCount ?? 0) + 1, | |
| 51 | firstIndex: current?.firstIndex ?? index, | |
| 52 | }); | |
| 53 | }); | |
| 54 | ||
| 55 | return Array.from(scopes, ([label, scope]) => ({ | |
| 56 | label, | |
| 57 | fileCount: scope.fileCount, | |
| 58 | firstIndex: scope.firstIndex, | |
| 59 | })) | |
| 60 | .toSorted( | |
| 61 | (left, right) => | |
| 62 | right.fileCount - left.fileCount || | |
| 63 | left.firstIndex - right.firstIndex || | |
| 64 | left.label.localeCompare(right.label), | |
| 65 | ) | |
| 66 | .slice(0, limit) | |
| 67 | .map(({ label, fileCount }) => ({ label, fileCount })); | |
| 68 | } | |
| 69 | ||
| 70 | export function selectChangedFilePreview( | |
| 71 | files: ReadonlyArray<TurnDiffFileChange>, | |
| 72 | limit = CHANGED_FILES_PREVIEW_FILE_LIMIT, | |
| 73 | ): TurnDiffFileChange[] { | |
| 74 | const selected: TurnDiffFileChange[] = []; | |
| 75 | const selectedPaths = new Set<string>(); | |
| 76 | const selectedScopes = new Set<string>(); | |
| 77 | ||
| 78 | for (const file of files) { | |
| 79 | const scope = changedFileScope(file.path); | |
| 80 | if (selectedScopes.has(scope)) { | |
| 81 | continue; | |
| 82 | } | |
| 83 | selected.push(file); | |
| 84 | selectedPaths.add(file.path); | |
| 85 | selectedScopes.add(scope); | |
| 86 | if (selected.length === limit) { | |
| 87 | return selected; | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | for (const file of files) { | |
| 92 | if (selectedPaths.has(file.path)) { | |
| 93 | continue; | |
| 94 | } | |
| 95 | selected.push(file); | |
| 96 | if (selected.length === limit) { | |
| 97 | break; | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | return selected; | |
| 102 | } |