| 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 | } | ||