diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ec667624..e736b4713 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Prevented focus rings in workspace connector dialogs from being clipped. [#1457](https://github.com/sourcebot-dev/sourcebot/pull/1457)
+- Prevented long file paths from overflowing the browse file header. [#1465](https://github.com/sourcebot-dev/sourcebot/pull/1465)
## [5.1.2] - 2026-07-16
diff --git a/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanelClient.tsx b/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanelClient.tsx
index 6368bacf1..48a157967 100644
--- a/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanelClient.tsx
+++ b/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanelClient.tsx
@@ -175,14 +175,17 @@ export const CodePreviewPanelClient = ({ path, repoName, revisionName, previewRe
return (
<>
-
-
- {fileWebUrl && (
+
+ {isFileSourcePending ? (
+
+ ) : fileWebUrl ? (
Open in {codeHostInfo.codeHostName}
- )}
+ ) : null}
{!previewRef && (
diff --git a/packages/web/src/app/(app)/browse/components/pureFileTreePanel.tsx b/packages/web/src/app/(app)/browse/components/pureFileTreePanel.tsx
index dded766f5..da4a66ffb 100644
--- a/packages/web/src/app/(app)/browse/components/pureFileTreePanel.tsx
+++ b/packages/web/src/app/(app)/browse/components/pureFileTreePanel.tsx
@@ -86,7 +86,7 @@ export const PureFileTreePanel = ({ tree, openPaths, path, onTreeNodeClicked }:
return (
{renderedTree}
@@ -94,4 +94,3 @@ export const PureFileTreePanel = ({ tree, openPaths, path, onTreeNodeClicked }:
)
}
-
diff --git a/packages/web/src/app/(app)/components/pathHeader.tsx b/packages/web/src/app/(app)/components/pathHeader.tsx
index 1739fd724..924a35541 100644
--- a/packages/web/src/app/(app)/components/pathHeader.tsx
+++ b/packages/web/src/app/(app)/components/pathHeader.tsx
@@ -4,7 +4,7 @@ import { cn, getCodeHostInfoForRepo, truncateSha } from "@/lib/utils";
import Image from "next/image";
import { getBrowsePath } from "../browse/hooks/utils";
import { ChevronRight, MoreHorizontal } from "lucide-react";
-import { useCallback, useState, useMemo, useRef, useEffect } from "react";
+import { useCallback, useState, useMemo, useRef, useEffect, useLayoutEffect } from "react";
import { useToast } from "@/components/hooks/use-toast";
import {
DropdownMenu,
@@ -49,6 +49,8 @@ interface BreadcrumbSegment {
};
}
+const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
+
export const PathHeader = ({
repo,
path,
@@ -110,12 +112,14 @@ export const PathHeader = ({
}, [path, pathHighlightRange]);
// Calculate which segments should be visible based on available space
- useEffect(() => {
+ useIsomorphicLayoutEffect(() => {
const measureSegments = () => {
if (!containerRef.current || !breadcrumbsRef.current) return;
const containerWidth = containerRef.current.offsetWidth;
const availableWidth = containerWidth - 40; // Reserve space for copy button and padding
+ const collapsedPrefixWidth = 40; // Ellipsis button + separator
+ const fileIconWidth = isFileIconVisible ? 20 : 0; // Icon + right margin
// Create a temporary element to measure segment widths
const tempElement = document.createElement('div');
@@ -125,27 +129,38 @@ export const PathHeader = ({
tempElement.className = 'font-mono text-sm';
document.body.appendChild(tempElement);
- let totalWidth = 0;
+ const segmentWidths = breadcrumbSegments.map((segment) => {
+ tempElement.textContent = segment.name;
+ return tempElement.offsetWidth;
+ });
+
+ const fullBreadcrumbWidth = segmentWidths.reduce((width, segmentWidth) => width + segmentWidth, fileIconWidth)
+ + Math.max(0, breadcrumbSegments.length - 1) * 16;
+
let visibleCount = breadcrumbSegments.length;
- // Start from the end (most important segments) and work backwards
- for (let i = breadcrumbSegments.length - 1; i >= 0; i--) {
- const segment = breadcrumbSegments[i];
- tempElement.textContent = segment.name;
- const segmentWidth = tempElement.offsetWidth;
- const separatorWidth = i < breadcrumbSegments.length - 1 ? 16 : 0; // ChevronRight width
-
- if (totalWidth + segmentWidth + separatorWidth > availableWidth && i > 0) {
- // If adding this segment would overflow and it's not the last segment
- visibleCount = breadcrumbSegments.length - i;
- // Add width for ellipsis dropdown (approximately 24px)
- if (visibleCount < breadcrumbSegments.length) {
- totalWidth += 40; // Ellipsis button + separator
+ if (fullBreadcrumbWidth > availableWidth) {
+ let visibleWidth = fileIconWidth;
+ visibleCount = 0;
+
+ // Keep the largest suffix that fits alongside the collapsed-prefix control.
+ for (let i = breadcrumbSegments.length - 1; i >= 0; i--) {
+ const separatorWidth = visibleCount > 0 ? 16 : 0;
+ const candidateWidth = visibleWidth + segmentWidths[i] + separatorWidth;
+ const prefixWidth = i > 0 ? collapsedPrefixWidth : 0;
+
+ if (candidateWidth + prefixWidth > availableWidth) {
+ // The final segment is always visible. It may truncate when there
+ // is not enough room for it and the fixed controls by themselves.
+ if (visibleCount === 0) {
+ visibleCount = 1;
+ }
+ break;
}
- break;
- }
- totalWidth += segmentWidth + separatorWidth;
+ visibleWidth = candidateWidth;
+ visibleCount++;
+ }
}
document.body.removeChild(tempElement);
@@ -160,7 +175,7 @@ export const PathHeader = ({
}
return () => resizeObserver.disconnect();
- }, [breadcrumbSegments]);
+ }, [breadcrumbSegments, isFileIconVisible]);
const hiddenSegments = useMemo(() => {
if (visibleSegmentCount === null || visibleSegmentCount >= breadcrumbSegments.length) {
@@ -249,14 +264,20 @@ export const PathHeader = ({
{breadcrumbSegments.length > 0 && (
ยท
)}
-
-
+
+
{hiddenSegments.length > 0 && (
<>