feat(web): add git history view#1150
Conversation
Adds a GitHub-style commit row below the path header on the file browse view, showing the author, message, short SHA, and relative date for the most recent commit that touched the file. A no-op History button is included as a placeholder for the upcoming file-history view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parse Co-authored-by trailers from the commit body and display an AvatarGroup with the combined author list, falling back to an overflow count when more than two. Add a toggle button next to the commit message to reveal the full commit body inline. Upgrades the Avatar ui component to include AvatarGroup, AvatarGroupCount, and AvatarBadge primitives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend the browse URL scheme to support /-/commits/<path> as a third pathType alongside blob and tree. Empty paths are permitted so the same route can later serve repo-level history. The History button in the commit header now links to this route via getBrowsePath. The page renders a placeholder for the commits pathType; the actual commit list panel will follow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Render a paginated commit history list when pathType is commits. The view reuses PathHeader for the "History for <repo> / <path>" subheader, groups commits by local date with sticky section headers, and shows Previous/Next links driven by a page query param. Each row renders the commit message, co-authors, short SHA, copy action, view-code-at-commit, and view-repo-at-commit links. When on the last page the list ends with an "End of commit history" marker. Refactor the author parsing, avatar group, body toggle, and body panel out of commitHeader into commitAuthors.ts and commitParts.tsx so the new CommitRow can reuse them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
List unique commit authors scoped to a ref and optional path, sorted by commit count descending. Backed by git shortlog -sne for a native walk that emits one line per author rather than per commit, which keeps the response small even for files with long histories. The route is exposed in the public API under the Git tag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The history subheader gets an "All users" dropdown that filters commits by author. The top 100 authors (by commit count) are fetched via listCommitAuthors and shown in a Popover + cmdk Command list with a search input, checkmark on the selected author, and a "View commits for all users" footer to clear. A "Filter on author <input>" row appears at the top whenever search is non-empty, acting as an escape valve for authors outside the top 100. The filter survives pagination by threading the author query param through CommitsPagination. Duplicate entries from the same email (git shortlog groups by full author string, so name-variant spellings split into multiple rows) are collapsed client-side with the name variant having the most commits winning as canonical. Document listCommits's --author and --grep as POSIX BRE regex and move the literal-escape responsibility onto the caller; CommitsPanel escapes the selected author via a BRE-safe helper before passing to git. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a date range dropdown next to the author filter using shadcn's range Calendar in a Popover. URL state is `?since=YYYY-MM-DD&until=YYYY-MM-DD` so ranges are shareable. Two clicks are required to form a range even when one is already selected — the component tracks an in-progress draft locally and intercepts react-day-picker v9's "adjust" behavior that would otherwise commit a new range in a single click. Single-day ranges require two clicks of the same date. The upper bound is made inclusive by appending end-of-day time before passing to git log. `since` also gets explicit midnight time to sidestep git's approxidate parser, which silently mishandles some bare YYYY-MM-DD forms. Future dates are disabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
This comment has been minimized.
This comment has been minimized.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughAdds a commit history feature to the code browser (per-file/latest commit header, paginated history, author and date filters) and a new public endpoint Changes
Sequence DiagramsequenceDiagram
actor User
participant BrowseUI as Browse Page<br/>(Commits view)
participant AuthorAPI as /api/commits/authors
participant CommitsAPI as /api/commits
participant GitService as Git Service<br/>(listCommitAuthors / listCommits)
User->>BrowseUI: Open history with repo/ref/path + filters
BrowseUI->>AuthorAPI: GET ?repo=X&ref=Y&path=Z&page=1&perPage=...
AuthorAPI->>GitService: listCommitAuthors(repo, ref, path, maxCount, skip)
GitService-->>AuthorAPI: authors[] + totalCount
AuthorAPI-->>BrowseUI: 200 + authors + X-Total-Count + Link
BrowseUI->>CommitsAPI: GET ?repo=X&author=A&since=S&until=U&page=N&perPage=M
CommitsAPI->>GitService: listCommits(query params)
GitService-->>CommitsAPI: commits[] + totalCount
CommitsAPI-->>BrowseUI: 200 + commits + X-Total-Count
BrowseUI->>BrowseUI: Group by day, render rows, show pagination
User->>BrowseUI: Select author/date or paginate
BrowseUI->>BrowseUI: Update URL params, refetch
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
License Audit
Weak Copyleft Packages (informational)
Resolved Packages (10)
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (7)
packages/web/src/openapi/publicApiSchemas.ts (1)
1-58: LGTM: OpenAPI schema exports for commit authors are consistent.The added public schemas are well-structured:
- query schema (for params),
- author object schema,
- response as an array of authors.
Please just make sure the route handler at
GET /api/commits/authorswires these exact schemas when generating/validating OpenAPI responses/requests (so docs and runtime stay aligned).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/openapi/publicApiSchemas.ts` around lines 1 - 58, Ensure the GET /api/commits/authors route handler uses the exported OpenAPI schemas: wire publicListCommitAuthorsQuerySchema for request query validation, publicCommitAuthorSchema for individual author objects, and publicListCommitAuthorsResponseSchema for the response body/schema in the OpenAPI generation and any runtime validation middleware (look for the route handler referencing GET /api/commits/authors and update the OpenAPI/validator config there to reference these three symbols).packages/web/src/components/ui/avatar.tsx (2)
8-26: Potential ref regression: wrapper drops/changesrefbehavior.
Avataris now a plain function wrapper aroundAvatarPrimitive.Rootwith noforwardRef. If any call sites rely on passingrefto<Avatar />(common for focus/scroll measurement), that ref may no longer work as before.Please verify (1) whether
<Avatar />is used withref={...}anywhere, and (2) whether React 19 in this repo actually treatsrefas a normal prop for function components with the current TS setup.If you don’t intend to support refs, consider changing the prop type to
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>to prevent accidental ref usage.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/components/ui/avatar.tsx` around lines 8 - 26, The Avatar wrapper drops ref forwarding and may break callers relying on ref; update Avatar to either forward refs to AvatarPrimitive.Root using React.forwardRef (preserving ref behavior for focus/measurement) or change the prop type to React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> to explicitly disallow refs; locate the Avatar function and modify its signature and export to use React.forwardRef (forward the ref into AvatarPrimitive.Root) or adjust the props type as suggested, and ensure the data-size/data-slot/className spread remain unchanged when applying the fix.
57-70: Badge sizing depends on group data on the ancestor.
AvatarBadgeusesgroup-data-[size=...] /avatar:*selectors and assumes it’s rendered under an element withdata-sizeandclass="group/avatar". This is likely correct if call sites nest<AvatarBadge />inside<Avatar />, but it’s worth ensuring the intended structure is documented/consistent.If any call sites render
AvatarBadgeoutsideAvatar, badge sizing could break silently.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/components/ui/avatar.tsx` around lines 57 - 70, AvatarBadge relies on ancestor group data selectors (group-data-[size=...]/avatar and data-size on an element with class "group/avatar") so badge sizing will break if it's rendered outside Avatar; update AvatarBadge to either accept an explicit size prop (e.g., size: "sm" | "default" | "lg") and apply the corresponding size classes when provided, or add a runtime check in AvatarBadge (useEffect) that warns when no ancestor with data-size and class "group/avatar" is found so callers know to nest it inside Avatar; also add a short doc comment on AvatarBadge and update Avatar to pass size down if it renders AvatarBadge.packages/web/src/features/git/listCommitsApi.ts (1)
78-107: Check correctness of--author/--grepoption ordering and escaping assumptions.The implementation correctly documents that
--authorand--grepare interpreted as git regex (POSIX BRE + GNU extensions) and that callers must escape metacharacters for literal matching.One concern to double-check: you currently build
sharedOptionsintoObject.entries(...), and while JS preserves insertion order, the presence/absence of author vs query can alter the relative order of--author/--grepvs--regexp-ignore-case. If ordering ever matters for parsing, it could be brittle. Tests cover some ordering, but not all combinations.At minimum, ensure there’s test coverage for “author only”, “query only”, and “author+query+since/until” with exact argv order expectations where it matters.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/features/git/listCommitsApi.ts` around lines 78 - 107, The ordering of --author/--grep vs their --regexp-ignore-case flag is brittle because sharedOptions relies on Object.entries insertion order; change the arg construction in listCommitsApi (the sharedOptions/logArgs logic) to explicitly append flags in a stable sequence (e.g., if author present push '--author=...' then push '--regexp-ignore-case', likewise for grep) rather than iterating Object.entries, and add unit tests for the three scenarios ("author only", "query only", and "author+query+since/until") asserting the exact logArgs/argv order produced by the function so regressions are caught.packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel.tsx (1)
16-30: Performance risk: extra git log call per code preview render.
codePreviewPanelnow issues an additionallistCommits(..., maxCount: 1)request for the currentpathandrevisionName. Depending on how manyCodePreviewPanelinstances render per page, this could become a significant load on git operations.If this component is rendered frequently (e.g., multiple open files, client transitions, pagination), consider:
- caching the “latest commit for this (repo, path, ref)” at the API or request layer, or
- computing it in a higher-level data fetch and passing it down.
Not necessarily blocker, but worth watching.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/app/`(app)/browse/[...path]/components/codePreviewPanel.tsx around lines 16 - 30, CodePreviewPanel currently issues an additional listCommits(...) call per render (using path and revisionName) which can be heavy; to fix, remove or memoize that call by either (1) moving the latest-commit lookup out of CodePreviewPanel into a higher-level loader or parent component and pass the commit data down, or (2) add a short-lived cache in the request layer/shared fetch helper so repeated listCommits(repo, path, ref, maxCount:1) requests reuse results; update CodePreviewPanel to use the already-fetched commit data or the cached fetch together with the existing getFileSource and getRepoInfoByName calls to avoid per-instance git log calls.packages/web/src/app/(app)/browse/[...path]/components/commitsPagination.tsx (1)
43-59: Prefer<button disabled>over<span aria-disabled="true">for disabled prev/next.A native disabled
<button>is keyboard-skipped automatically and is consistently announced as disabled by screen readers; the current<span aria-disabled="true">still receives focus traversal in some flows and relies entirely on visual styling. Minor a11y polish.♻️ Suggested refactor
- ) : ( - <span className={cn(disabledClass)} aria-disabled="true"> - <ChevronLeft className="h-4 w-4" /> - Previous - </span> - )} + ) : ( + <button type="button" disabled className={cn(disabledClass)}> + <ChevronLeft className="h-4 w-4" /> + Previous + </button> + )} @@ - ) : ( - <span className={cn(disabledClass)} aria-disabled="true"> - Next - <ChevronRight className="h-4 w-4" /> - </span> - )} + ) : ( + <button type="button" disabled className={cn(disabledClass)}> + Next + <ChevronRight className="h-4 w-4" /> + </button> + )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/app/`(app)/browse/[...path]/components/commitsPagination.tsx around lines 43 - 59, Replace the non-interactive disabled spans used for Prev/Next with native disabled buttons so they are skipped by keyboard and announced by assistive tech: where the code currently renders <span className={cn(disabledClass)} aria-disabled="true"> for the previous and next controls, change those branches to render <button type="button" disabled className={cn(disabledClass)}> with the same children (ChevronLeft/ChevronRight and text). Keep the enabled branches using Link (Link href={buildHref(page + 1, extraParams)} className={linkClass}) and preserve linkClass, disabledClass, hasNext/hasPrev (and page/extraParams) usage. Ensure no href is added to the disabled button and retain the icon order/structure.packages/web/src/app/(app)/browse/[...path]/page.tsx (1)
78-99: Consider whether to addsearchParamsvalidation, but note validation already exists at the API boundary.In Next.js 16 App Router,
searchParamsvalues arestring | string[] | undefined. The current type narrowing tostringdoesn't account for repeated query parameters (e.g.,?page=1&page=2arrives as an array), which would be silently coerced byparseIntor template strings. However, similar page components in the codebase (signup, redeem, onboard) follow the same direct passthrough pattern. SinceCommitsPanelultimately calls the API route atpackages/web/src/app/api/(server)/commits/route.ts, which validates vialistCommitsQueryParamsSchema, the validation already occurs at the appropriate boundary. Adding Zod validation here would be stricter but would diverge from the established pattern where validation is centralized at API routes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/app/`(app)/browse/[...path]/page.tsx around lines 78 - 99, The searchParams values in BrowsePage (BrowsePageProps -> searchParams) can be string|string[]|undefined; update the coercion logic so you handle string[] cases before parsing: for each param (searchParams.page, .author, .since, .until) normalize to a single string by taking the first element when Array.isArray(...) (or otherwise converting to string) and then parse page with parseInt and assign author/since/until as undefined when empty; keep validation at the API boundary but ensure BrowsePage doesn't silently coerce arrays (refer to BrowsePage, searchParams, page, author, since, until, and getBrowseParamsFromPathParam).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Around line 8-12: The Unreleased entry under "## [Unreleased]" -> "### Added"
is not a single sentence; edit the single bullet (the line starting "Added a
GitHub-style git history view to the code browser: ...") so it becomes one
sentence (for example by joining the clause "Also exposes GET
/api/commits/authors in the public API." to the previous sentence with
appropriate punctuation or conjunction), ensuring the entire bullet is a single
grammatically correct sentence.
In `@packages/web/src/app/`(app)/browse/[...path]/components/commitsPanel.tsx:
- Around line 37-56: The author list is being fetched without date bounds while
commits are filtered by since/until; update the listCommitAuthors call (the call
named listCommitAuthors) to include the same since/until parameters used for
listCommits (sinceForGit and untilForGit) so the "top authors" reflect the
current date window for the given repo/path/ref (path, revisionName), or
explicitly document/guard the current behavior if you intentionally want authors
across the entire ref.
In `@packages/web/src/app/`(app)/browse/[...path]/components/dateFilter.tsx:
- Around line 22-29: parseLocalDate currently accepts structurally-valid but
semantically-invalid dates (e.g., "2024-13-45") which new Date(...) silently
rolls into a different date; to fix it, after extracting year/month/day in
parseLocalDate validate by constructing the Date and then verifying
date.getFullYear() === year, date.getMonth() === month - 1 and date.getDate()
=== day, returning undefined if any check fails so only true calendar-valid
dates are accepted.
- Around line 66-91: The popover's month state ("month"/setMonth) is only
initialized from fromDate and never updated when fromDate changes externally, so
the calendar can open on the wrong month; update month whenever the popover is
opened or when fromDate changes by adding an effect that runs on [isOpen,
fromDate] (or separate effects) and calls setMonth(fromDate or
selectedRange.from) to resync the visible month with the URL-derived
fromDate/draftRange; reference the existing month/setMonth, fromDate, isOpen,
selectedRange and draftRange to locate where to add this sync.
In `@packages/web/src/features/git/listCommitAuthorsApi.ts`:
- Around line 85-107: The catch block in listCommitAuthorsApi currently maps git
ref errors to unexpectedError or throws, causing 500s for client mistakes;
update it to import and return the existing service errors (invalidGitRef and
unresolvedGitRef) instead of unexpectedError/throwing for ref-related messages:
detect messages like 'ambiguous argument', 'unknown revision', 'bad revision',
'invalid object name' (and any other git ref resolution strings) and return
invalidGitRef(ref) or unresolvedGitRef(ref) as appropriate; keep the existing
handling for 'not a git repository' but ensure other non-ref errors still
surface as unexpectedError, and remove the final throw so the function
consistently returns a service error type.
- Around line 56-84: The code parses shortlog into `all` then slices it into
`authors` and returns `totalCount = all.length`, which lets name-variants for
the same email skew paging; instead, dedupe authors by lowercased email on the
backend before computing totals and slicing: build a map keyed by
email.toLowerCase() (merge commit counts and choose a canonical name—e.g.,
first-seen), produce a deduped array sorted by commitCount desc, set
`totalCount` to deduped.length, then slice that array with `skip`/`maxCount` and
return those results; update the logic around `all`, `totalCount`, and `authors`
in the function that calls `git.raw` (the code block using `lineRegex` and
pushing into `all`) so the returned page matches UI dedupe behavior.
---
Nitpick comments:
In `@packages/web/src/app/`(app)/browse/[...path]/components/codePreviewPanel.tsx:
- Around line 16-30: CodePreviewPanel currently issues an additional
listCommits(...) call per render (using path and revisionName) which can be
heavy; to fix, remove or memoize that call by either (1) moving the
latest-commit lookup out of CodePreviewPanel into a higher-level loader or
parent component and pass the commit data down, or (2) add a short-lived cache
in the request layer/shared fetch helper so repeated listCommits(repo, path,
ref, maxCount:1) requests reuse results; update CodePreviewPanel to use the
already-fetched commit data or the cached fetch together with the existing
getFileSource and getRepoInfoByName calls to avoid per-instance git log calls.
In
`@packages/web/src/app/`(app)/browse/[...path]/components/commitsPagination.tsx:
- Around line 43-59: Replace the non-interactive disabled spans used for
Prev/Next with native disabled buttons so they are skipped by keyboard and
announced by assistive tech: where the code currently renders <span
className={cn(disabledClass)} aria-disabled="true"> for the previous and next
controls, change those branches to render <button type="button" disabled
className={cn(disabledClass)}> with the same children (ChevronLeft/ChevronRight
and text). Keep the enabled branches using Link (Link href={buildHref(page + 1,
extraParams)} className={linkClass}) and preserve linkClass, disabledClass,
hasNext/hasPrev (and page/extraParams) usage. Ensure no href is added to the
disabled button and retain the icon order/structure.
In `@packages/web/src/app/`(app)/browse/[...path]/page.tsx:
- Around line 78-99: The searchParams values in BrowsePage (BrowsePageProps ->
searchParams) can be string|string[]|undefined; update the coercion logic so you
handle string[] cases before parsing: for each param (searchParams.page,
.author, .since, .until) normalize to a single string by taking the first
element when Array.isArray(...) (or otherwise converting to string) and then
parse page with parseInt and assign author/since/until as undefined when empty;
keep validation at the API boundary but ensure BrowsePage doesn't silently
coerce arrays (refer to BrowsePage, searchParams, page, author, since, until,
and getBrowseParamsFromPathParam).
In `@packages/web/src/components/ui/avatar.tsx`:
- Around line 8-26: The Avatar wrapper drops ref forwarding and may break
callers relying on ref; update Avatar to either forward refs to
AvatarPrimitive.Root using React.forwardRef (preserving ref behavior for
focus/measurement) or change the prop type to
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> to explicitly
disallow refs; locate the Avatar function and modify its signature and export to
use React.forwardRef (forward the ref into AvatarPrimitive.Root) or adjust the
props type as suggested, and ensure the data-size/data-slot/className spread
remain unchanged when applying the fix.
- Around line 57-70: AvatarBadge relies on ancestor group data selectors
(group-data-[size=...]/avatar and data-size on an element with class
"group/avatar") so badge sizing will break if it's rendered outside Avatar;
update AvatarBadge to either accept an explicit size prop (e.g., size: "sm" |
"default" | "lg") and apply the corresponding size classes when provided, or add
a runtime check in AvatarBadge (useEffect) that warns when no ancestor with
data-size and class "group/avatar" is found so callers know to nest it inside
Avatar; also add a short doc comment on AvatarBadge and update Avatar to pass
size down if it renders AvatarBadge.
In `@packages/web/src/features/git/listCommitsApi.ts`:
- Around line 78-107: The ordering of --author/--grep vs their
--regexp-ignore-case flag is brittle because sharedOptions relies on
Object.entries insertion order; change the arg construction in listCommitsApi
(the sharedOptions/logArgs logic) to explicitly append flags in a stable
sequence (e.g., if author present push '--author=...' then push
'--regexp-ignore-case', likewise for grep) rather than iterating Object.entries,
and add unit tests for the three scenarios ("author only", "query only", and
"author+query+since/until") asserting the exact logArgs/argv order produced by
the function so regressions are caught.
In `@packages/web/src/openapi/publicApiSchemas.ts`:
- Around line 1-58: Ensure the GET /api/commits/authors route handler uses the
exported OpenAPI schemas: wire publicListCommitAuthorsQuerySchema for request
query validation, publicCommitAuthorSchema for individual author objects, and
publicListCommitAuthorsResponseSchema for the response body/schema in the
OpenAPI generation and any runtime validation middleware (look for the route
handler referencing GET /api/commits/authors and update the OpenAPI/validator
config there to reference these three symbols).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cde158a4-5333-48c3-89d9-d5b38c3a62c9
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (25)
CHANGELOG.mddocs/api-reference/sourcebot-public.openapi.jsondocs/docs.jsonpackages/web/package.jsonpackages/web/src/app/(app)/browse/[...path]/components/authorFilter.tsxpackages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel.tsxpackages/web/src/app/(app)/browse/[...path]/components/commitAuthors.tspackages/web/src/app/(app)/browse/[...path]/components/commitHeader.tsxpackages/web/src/app/(app)/browse/[...path]/components/commitParts.tsxpackages/web/src/app/(app)/browse/[...path]/components/commitRow.tsxpackages/web/src/app/(app)/browse/[...path]/components/commitsPagination.tsxpackages/web/src/app/(app)/browse/[...path]/components/commitsPanel.tsxpackages/web/src/app/(app)/browse/[...path]/components/dateFilter.tsxpackages/web/src/app/(app)/browse/[...path]/page.tsxpackages/web/src/app/(app)/browse/hooks/utils.tspackages/web/src/app/api/(server)/commits/authors/route.tspackages/web/src/components/ui/avatar.tsxpackages/web/src/components/ui/calendar.tsxpackages/web/src/features/git/index.tspackages/web/src/features/git/listCommitAuthorsApi.tspackages/web/src/features/git/listCommitsApi.test.tspackages/web/src/features/git/listCommitsApi.tspackages/web/src/features/git/schemas.tspackages/web/src/openapi/publicApiDocument.tspackages/web/src/openapi/publicApiSchemas.ts
Adds a new History tab to the bottom panel alongside Explore. The tab is toggled via shift+mod+h with the existing collapse-on-same/switch-on-other semantics, and the active tab is indicated with an underline using LowProfileTabsTrigger (now accepts an optional className). The history panel itself is a client-side infinite-scroll list (react-query useInfiniteQuery + IntersectionObserver) that hits a new client-side listCommits wrapper. Each row reuses AuthorsAvatarGroup, the co-author parsing, and the new shared CommitActionLink primitive for the view-code-at-commit / view-repo-at-commit actions. CommitRow was refactored to use the same primitive. The previous CommitHeader rendered above each open file is removed. A compact summary of the latest commit now appears in the bottom panel header (right side) whenever the panel is collapsed, so the file/folder's last commit stays glanceable without consuming a full row above the code. When the panel is expanded the right side hosts the View-full-history and Hide buttons. Shared commit utilities (commitAuthors.ts, commitParts.tsx) moved up from browse/[...path]/components/ to browse/components/ so both the existing commits view and the new bottom-panel components import them without crossing route boundaries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
packages/web/src/app/(app)/browse/components/bottomPanel.tsx (1)
47-52: Minor: memoizefullHistoryHref.Recomputed on every render; cheap but easy to stabilize:
♻️ Proposed tweak
- const { repoName, revisionName, path } = useBrowseParams(); - const fullHistoryHref = getBrowsePath({ - repoName, - revisionName, - path, - pathType: 'commits', - }); + const { repoName, revisionName, path } = useBrowseParams(); + const fullHistoryHref = useMemo( + () => getBrowsePath({ repoName, revisionName, path, pathType: 'commits' }), + [repoName, revisionName, path], + );(Add
useMemoto the existing react import.)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/app/`(app)/browse/components/bottomPanel.tsx around lines 47 - 52, fullHistoryHref is recomputed on every render; wrap the call to getBrowsePath in a useMemo to stabilize the value. Import useMemo from React (add to the existing react import) and replace the direct assignment of fullHistoryHref with a useMemo(() => getBrowsePath({ repoName, revisionName, path, pathType: 'commits' }), [repoName, revisionName, path]); so the memo depends on repoName, revisionName, and path and uses the existing getBrowsePath reference in bottomPanel.tsx.packages/web/src/app/(app)/browse/components/commitAuthors.ts (1)
5-13: Co-author trailers with leading whitespace will be missed.Some commit conventions (and tools) emit indented trailers, e.g. when a commit body is wrapped/quoted. The regex anchors
^directly toco-authored-by:and won't matchCo-authored-by: …. If you want to be lenient, allow optional leading whitespace:♻️ Proposed tweak
- const regex = /^co-authored-by:\s*(.+?)\s*<(.+?)>\s*$/gim; + const regex = /^[ \t]*co-authored-by:\s*(.+?)\s*<(.+?)>\s*$/gim;Not a blocker — standard
git commit -s/--trailer-emitted trailers will already match.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/app/`(app)/browse/components/commitAuthors.ts around lines 5 - 13, The parseCoAuthors function's regex currently anchors to the line start and misses indented trailers; update the regex used in parseCoAuthors to allow optional leading whitespace (e.g. prefix with \s* after ^) so lines like " Co-authored-by: Name <email>" are matched; keep the existing case-insensitive and global flags and ensure the rest of the capture groups (name/email) remain unchanged so coAuthors.push({ name: match[1].trim(), email: match[2].trim() }) still works.packages/web/src/app/(app)/browse/components/historyPanel.tsx (1)
51-66: Observer is reattached on everyisFetchingNextPageflip.Including
isFetchingNextPagein the effect deps means the observer is destroyed and recreated each time a page starts/finishes fetching. It works, but you can drop it from the deps and read the latest value via a ref (or justdata?.pages.length-style guard inside the callback) to avoid the churn. Optional.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/app/`(app)/browse/components/historyPanel.tsx around lines 51 - 66, The effect re-creates the IntersectionObserver whenever isFetchingNextPage flips; remove isFetchingNextPage from the dependency array and read its latest value via a ref instead. Create a ref like isFetchingNextPageRef, update isFetchingNextPageRef.current whenever isFetchingNextPage changes, then in the IntersectionObserver callback check entries[0].isIntersecting && !isFetchingNextPageRef.current before calling fetchNextPage; keep hasNextPage and fetchNextPage (and sentinelRef) in the effect deps so the observer is only recreated when those truly change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/web/src/app/`(app)/browse/[...path]/components/commitRow.tsx:
- Around line 52-56: The onCopySha handler calls
navigator.clipboard.writeText(commit.hash) but doesn't await it, so the success
toast is shown even if the promise rejects; update the onCopySha function to
await the writeText Promise inside a try/catch (or use .then/.catch) so you only
call toast({ description: "✅ Copied commit SHA to clipboard" }) on success and
show an error toast on failure, and also guard for missing API (typeof
navigator.clipboard === "undefined") to surface a sensible error message; touch
the onCopySha function, navigator.clipboard.writeText, commit.hash and toast
references when implementing this.
In `@packages/web/src/app/`(app)/browse/[...path]/components/commitsPanel.tsx:
- Around line 70-128: The UI shows only "End of commit history" when filters
return zero commits; update the render logic in commitsPanel.tsx to detect
commits.length === 0 (or totalCount === 0) and render an explicit empty state
above the current End message instead of mapping groups: show a friendly message
(e.g., "No commits match these filters"), optional actionable guidance to clear
filters, and a control to reset AuthorFilter/DateFilter (or a callback prop) so
users can clear filter state; keep existing groups Map/isLastPage logic and
CommitRow rendering for the non-empty path.
- Around line 30-35: The sinceForGit and untilForGit timestamps are built
without timezone info so git interprets them in server-local time causing
day-shift bugs; update the code that constructs sinceForGit/untilForGit (the
values derived from parseLocalDate) to produce timezone-qualified ISO timestamps
(either convert the parsed local date to UTC and append 'Z' or include the local
offset) before sending to git so --since/--until are interpreted consistently
across client and server; ensure the transformation happens where sinceForGit
and untilForGit are defined and used.
In `@packages/web/src/app/`(app)/browse/components/bottomPanel.tsx:
- Around line 132-138: The ExternalLink icon next to the in-app route
fullHistoryHref is misleading; update the UI in bottomPanel.tsx so the button
reflects internal navigation by replacing ExternalLink with a semantic internal
icon (e.g., History or ListOrdered) and update the corresponding import, or if
you intended to open the link in a new tab, keep ExternalLink but add
target="_blank" rel="noopener" to the Link; ensure the change touches the Button
asChild / Link element and the import statement for the icon (ExternalLink →
History/ListOrdered) so the icon matches the chosen behavior.
In `@packages/web/src/app/`(app)/browse/components/historyPanel.tsx:
- Around line 91-98: The HistoryPanel is not passing pathType into each
HistoryRow, so per-row file actions can't know if the current path is a blob or
tree; update the map that renders HistoryRow (where allCommits is iterated) to
include the pathType prop (alongside repoName and path) so HistoryRow can use it
to build the correct “View code at this commit” link; locate the rendering block
that calls <HistoryRow key={commit.hash} commit={commit} repoName={repoName}
path={path} /> and add pathType={pathType} (getting pathType from the same
useBrowseParams context you already use elsewhere).
In `@packages/web/src/app/`(app)/browse/components/historyRow.tsx:
- Around line 20-37: The "View code at this commit" action can generate blob
URLs for directory paths because hasFilePath only checks path !== '' && path !==
'/' and viewCodeHref always uses pathType: 'blob'; update HistoryRow so it only
shows the file action when the row's pathType is 'blob' (either by threading
pathType from useBrowseParams through HistoryPanel -> HistoryRow, or by
computing a per-row pathType before building viewCodeHref), and ensure
viewCodeHref uses that pathType (instead of always 'blob'); keep viewRepoHref as
tree. Adjust rendering to hide the FileCode icon/label when pathType !== 'blob'
to avoid invalid /-/blob/... URLs.
In `@packages/web/src/app/`(app)/browse/components/latestCommitInfo.tsx:
- Around line 15-39: The component currently hides the header when the query is
loading or errors because it only checks commit; update the useQuery call in
latestCommitInfo.tsx to destructure isLoading and isError (e.g., const { data:
commit, isLoading, isError } = useQuery(...)) and then render appropriate
fallbacks instead of returning null: show a small muted placeholder like
"Loading latest commit…" when isLoading, and "Couldn't load latest commit"
(styled muted) when isError; keep returning null only when commit === null (no
commits). Ensure any existing logic that computes authors via
getCommitAuthors(commit) remains guarded by commit and the useMemo dependency.
---
Nitpick comments:
In `@packages/web/src/app/`(app)/browse/components/bottomPanel.tsx:
- Around line 47-52: fullHistoryHref is recomputed on every render; wrap the
call to getBrowsePath in a useMemo to stabilize the value. Import useMemo from
React (add to the existing react import) and replace the direct assignment of
fullHistoryHref with a useMemo(() => getBrowsePath({ repoName, revisionName,
path, pathType: 'commits' }), [repoName, revisionName, path]); so the memo
depends on repoName, revisionName, and path and uses the existing getBrowsePath
reference in bottomPanel.tsx.
In `@packages/web/src/app/`(app)/browse/components/commitAuthors.ts:
- Around line 5-13: The parseCoAuthors function's regex currently anchors to the
line start and misses indented trailers; update the regex used in parseCoAuthors
to allow optional leading whitespace (e.g. prefix with \s* after ^) so lines
like " Co-authored-by: Name <email>" are matched; keep the existing
case-insensitive and global flags and ensure the rest of the capture groups
(name/email) remain unchanged so coAuthors.push({ name: match[1].trim(), email:
match[2].trim() }) still works.
In `@packages/web/src/app/`(app)/browse/components/historyPanel.tsx:
- Around line 51-66: The effect re-creates the IntersectionObserver whenever
isFetchingNextPage flips; remove isFetchingNextPage from the dependency array
and read its latest value via a ref instead. Create a ref like
isFetchingNextPageRef, update isFetchingNextPageRef.current whenever
isFetchingNextPage changes, then in the IntersectionObserver callback check
entries[0].isIntersecting && !isFetchingNextPageRef.current before calling
fetchNextPage; keep hasNextPage and fetchNextPage (and sentinelRef) in the
effect deps so the observer is only recreated when those truly change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1eceb3fc-ea9a-4bda-904a-e6f0bae59728
📒 Files selected for processing (12)
packages/web/src/app/(app)/browse/[...path]/components/commitRow.tsxpackages/web/src/app/(app)/browse/[...path]/components/commitsPanel.tsxpackages/web/src/app/(app)/browse/browseStateProvider.tsxpackages/web/src/app/(app)/browse/components/bottomPanel.tsxpackages/web/src/app/(app)/browse/components/commitAuthors.tspackages/web/src/app/(app)/browse/components/commitParts.tsxpackages/web/src/app/(app)/browse/components/historyPanel.tsxpackages/web/src/app/(app)/browse/components/historyRow.tsxpackages/web/src/app/(app)/browse/components/latestCommitInfo.tsxpackages/web/src/app/api/(client)/client.tspackages/web/src/components/ui/tab-switcher.tsxpackages/web/src/features/git/listCommitsApi.ts
✅ Files skipped from review due to trivial changes (2)
- packages/web/src/app/(app)/browse/browseStateProvider.tsx
- packages/web/src/features/git/listCommitsApi.ts
…dling The commits view URL pattern (/-/commits/<path>) doesn't carry whether the path is a file or a folder, so PathHeader was rendering folder paths with the file-icon last-segment treatment. Add a small getPathType helper backed by `git cat-file -t <ref>:<path>` and use it to pick the correct PathHeader behaviour. CommitRow and HistoryRow now gate the "view code at this commit" action on the same blob check so the file-only link doesn't appear on folder rows. Also fix a related path-handling bug: PathHeader's repo-name link called getBrowsePath with `path: '/'`, which encoded as `%2F` and parsed back as `path: '/'` — that leaked through as `git log -- /`, which git correctly rejects with "outside repository". Strip leading slashes both when generating and when parsing browse URLs so any path that comes through as a literal `/` resolves to the repo root. When clicking "View full history" the bottom panel now collapses on the same click, so the new full-view page renders without the panel still hovering on top of it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/web/src/features/git/getPathTypeApi.ts (1)
75-82: Preserve original error viacausewhen re-throwing.The wrapping
Errordiscards the original stack trace, which makes diagnosing unexpectedsimple-gitfailures harder in production logs. Pass the original error throughcause.♻️ Proposed refactor
- if (error instanceof Error) { - throw new Error( - `Failed to resolve path type for ${repoName}:${path}: ${error.message}`, - ); - } - throw new Error( - `Failed to resolve path type for ${repoName}:${path}: ${errorMessage}`, - ); + throw new Error( + `Failed to resolve path type for ${repoName}:${path}: ${errorMessage}`, + error instanceof Error ? { cause: error } : undefined, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/features/git/getPathTypeApi.ts` around lines 75 - 82, The thrown wrapper Errors in the path resolution logic lose the original error stack; update both throw sites in getPathTypeApi (the branch checking error instanceof Error and the fallback throwing with errorMessage) to include the original error as the cause (use the Error constructor's { cause: ... } option), so the original exception/stack is preserved when re-throwing from the function that resolves path type (the two throw new Error(...) lines).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/web/src/features/git/getPathTypeApi.ts`:
- Around line 75-82: The thrown wrapper Errors in the path resolution logic lose
the original error stack; update both throw sites in getPathTypeApi (the branch
checking error instanceof Error and the fallback throwing with errorMessage) to
include the original error as the cause (use the Error constructor's { cause:
... } option), so the original exception/stack is preserved when re-throwing
from the function that resolves path type (the two throw new Error(...) lines).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cf80b38b-52f4-4735-a2ec-50009ae93c1c
📒 Files selected for processing (8)
packages/web/src/app/(app)/browse/[...path]/components/commitRow.tsxpackages/web/src/app/(app)/browse/[...path]/components/commitsPanel.tsxpackages/web/src/app/(app)/browse/components/bottomPanel.tsxpackages/web/src/app/(app)/browse/components/historyPanel.tsxpackages/web/src/app/(app)/browse/components/historyRow.tsxpackages/web/src/app/(app)/browse/hooks/utils.tspackages/web/src/features/git/getPathTypeApi.tspackages/web/src/features/git/index.ts
✅ Files skipped from review due to trivial changes (2)
- packages/web/src/app/(app)/browse/components/historyRow.tsx
- packages/web/src/app/(app)/browse/components/bottomPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/web/src/features/git/index.ts
- packages/web/src/app/(app)/browse/components/historyPanel.tsx
- packages/web/src/app/(app)/browse/[...path]/components/commitsPanel.tsx
Fixes SOU-131
Screenshots:
Commit header (single author):

Commit header (multi-author):

Commit header w/ commit body expanded:

Commit history view (file):

Commit history view (repo root):

note: we aren't explicitly linking to this anywhere. A good place to surface this would probably be if we built a "repository landing page" that render's the root README.md.
Filter commit history by user:

Filter commit history by date range:

Bottom bar (collapsed):

Bottom bar (expanded on history):

Summary
Adds a GitHub-style git history experience to the code browser:
commitspathType —/browse/<repo>@<rev>/-/commits[/<path>]renders a paginated commit history view, grouped by date with sticky section headersThe author and date filters are threaded through a shareable URL (
?author=&since=&until=&page=), and a new public API endpointGET /api/commits/authorsis exposed under the Git tag.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation