fix(desktop): refine repository pagination

Signed-off-by: Evan Chen <evanc@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
This commit is contained in:
Evan Chen
2026-08-06 15:08:50 -07:00
co-authored by Codex
parent e9de8ecad1
commit ff58cf8db0
3 changed files with 75 additions and 22 deletions
+16 -16
View File
@@ -9,8 +9,7 @@ use serde::Serialize;
use std::time::UNIX_EPOCH;
use tauri::State;
// Bound eager snapshot payloads without truncating the repository tree. Later
// paths remain browseable without embedded content.
// Bound eager content without truncating the repository tree.
const MAX_EAGER_FILE_PREVIEWS: usize = 250;
#[cfg(test)]
@@ -260,16 +259,16 @@ fn parse_worktree_files(
output
.split('\0')
.filter(|path| !path.trim().is_empty())
.enumerate()
.filter_map(|(index, path)| {
.filter_map(|path| {
let full_path = repo_dir.join(path);
let metadata = std::fs::metadata(&full_path).ok()?;
if !metadata.is_file() {
return None;
}
metadata.is_file().then_some((path, full_path, metadata))
})
.enumerate()
.map(|(index, (path, full_path, metadata))| {
let size = Some(metadata.len());
let latest_commit = latest_commit_by_path.get(path).cloned();
Some(ProjectRepoFileInfo {
ProjectRepoFileInfo {
path: path.to_string(),
kind: "blob".to_string(),
size,
@@ -281,7 +280,7 @@ fn parse_worktree_files(
.map(|commit| commit.timestamp)
.or_else(|| path_modified_at(&full_path)),
latest_commit,
})
}
})
.collect()
}
@@ -325,21 +324,22 @@ fn parse_ls_tree(
output: &str,
latest_commit_by_path: &std::collections::HashMap<String, ProjectRepoCommitInfo>,
) -> Vec<ProjectRepoFileInfo> {
let mut blob_index = 0;
output
.lines()
.enumerate()
.filter_map(|(index, line)| {
.filter_map(|line| {
let (meta, path) = line.split_once('\t')?;
let mut parts = meta.split_whitespace();
let _mode = parts.next()?;
let kind = parts.next()?.to_string();
let _object = parts.next()?;
let size = parts.next().and_then(|value| value.parse::<u64>().ok());
let preview_content = if kind == "blob" && index < MAX_EAGER_FILE_PREVIEWS {
read_preview_content(repo_dir, path, size)
} else {
None
};
if kind == "blob" {
blob_index += 1;
}
let preview_content = (kind == "blob" && blob_index <= MAX_EAGER_FILE_PREVIEWS)
.then(|| read_preview_content(repo_dir, path, size))
.flatten();
Some(ProjectRepoFileInfo {
path: path.to_string(),
kind,
@@ -28,3 +28,54 @@ fn parse_ls_tree_keeps_paths_after_eager_preview_limit() {
Some("src/application.rs")
);
}
#[test]
fn parse_ls_tree_counts_only_blobs_toward_eager_preview_limit() {
let repo_dir = tempfile::tempdir().expect("create temporary repository");
std::fs::write(repo_dir.path().join("application.rs"), "fn main() {}")
.expect("write preview file");
let non_blob_entries = (0..MAX_EAGER_FILE_PREVIEWS)
.map(|index| {
format!(
"160000 commit {} -\tvendor/dependency-{index:03}",
"a".repeat(40)
)
})
.collect::<Vec<_>>()
.join("\n");
let output = format!(
"{non_blob_entries}\n100644 blob {} 12\tapplication.rs",
"b".repeat(40)
);
let files = parse_ls_tree(repo_dir.path(), &output, &std::collections::HashMap::new());
assert_eq!(
files
.last()
.and_then(|file| file.preview_content.as_deref()),
Some("fn main() {}")
);
}
#[test]
fn parse_worktree_files_counts_only_files_toward_eager_preview_limit() {
let repo_dir = tempfile::tempdir().expect("create temporary repository");
std::fs::create_dir(repo_dir.path().join("directory")).expect("create directory");
let paths = (0..MAX_EAGER_FILE_PREVIEWS)
.map(|index| {
let path = format!("file-{index:03}.txt");
std::fs::write(repo_dir.path().join(&path), "preview").expect("write preview file");
path
})
.collect::<Vec<_>>();
let output = std::iter::once("directory")
.chain(paths.iter().map(String::as_str))
.collect::<Vec<_>>()
.join("\0");
let files = parse_worktree_files(repo_dir.path(), &output, &std::collections::HashMap::new());
assert_eq!(files.len(), MAX_EAGER_FILE_PREVIEWS);
assert!(files.iter().all(|file| file.preview_content.is_some()));
}
@@ -649,6 +649,11 @@ export function RepositoryFilesPanel({
[currentPath, files],
);
const visibleEntries = entries.slice(0, visibleEntryCount);
const nextVisibleEntryCount = nextRepositoryEntryLimit(
visibleEntryCount,
entries.length,
);
const nextEntryCount = nextVisibleEntryCount - visibleEntries.length;
const latestCommit = snapshot?.latestCommit ?? null;
const knownLatestCommitProfile = React.useMemo(
() => profileForCommitAuthor(latestCommit, profiles),
@@ -918,10 +923,11 @@ export function RepositoryFilesPanel({
</div>
{entries.length > visibleEntries.length ? (
<div className="flex items-center justify-between gap-3 border-border/50 border-t px-4 py-3 text-2xs text-muted-foreground">
<span>
<span aria-live="polite">
Showing {visibleEntries.length} of {entries.length} entries.
</span>
<button
aria-label={`Show next ${nextEntryCount} entries, ${nextVisibleEntryCount} of ${entries.length} total`}
className="shrink-0 font-medium text-foreground hover:underline"
onClick={() =>
setVisibleEntryCount((current) =>
@@ -930,11 +936,7 @@ export function RepositoryFilesPanel({
}
type="button"
>
Show next{" "}
{Math.min(
REPOSITORY_ENTRY_PAGE_SIZE,
entries.length - visibleEntries.length,
)}
Show next {nextEntryCount}
</button>
</div>
) : null}