Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,17 @@ export const CodePreviewPanelClient = ({ path, repoName, revisionName, previewRe
return (
<>
<div className="flex flex-row py-1 px-2 items-center justify-between">
<PathHeader
path={path}
repo={repo}
revisionName={contentRef}
/>

{fileWebUrl && (
<div className="min-w-0 flex-1">
<PathHeader
path={path}
repo={repo}
revisionName={contentRef}
/>
</div>

{isFileSourcePending ? (
<Skeleton className="h-6 w-32 mx-2 rounded-md flex-shrink-0" />
) : fileWebUrl ? (
<a
href={fileWebUrl}
target="_blank"
Expand All @@ -196,7 +199,7 @@ export const CodePreviewPanelClient = ({ path, repoName, revisionName, previewRe
/>
<span className="text-sm font-medium">Open in {codeHostInfo.codeHostName}</span>
</a>
)}
) : null}
</div>
<Separator />
{!previewRef && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,11 @@ export const PureFileTreePanel = ({ tree, openPaths, path, onTreeNodeClicked }:

return (
<ScrollArea
className="h-full w-full overflow-auto p-0.5"
className="h-full w-full min-w-0 p-0.5"
ref={scrollAreaRef}
>
{renderedTree}
<ScrollBar orientation="horizontal" />
</ScrollArea>
)
}

83 changes: 55 additions & 28 deletions packages/web/src/app/(app)/components/pathHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -49,6 +49,8 @@ interface BreadcrumbSegment {
};
}

const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;

export const PathHeader = ({
repo,
path,
Expand Down Expand Up @@ -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');
Expand All @@ -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);
Expand All @@ -160,7 +175,7 @@ export const PathHeader = ({
}

return () => resizeObserver.disconnect();
}, [breadcrumbSegments]);
}, [breadcrumbSegments, isFileIconVisible]);

const hiddenSegments = useMemo(() => {
if (visibleSegmentCount === null || visibleSegmentCount >= breadcrumbSegments.length) {
Expand Down Expand Up @@ -249,14 +264,20 @@ export const PathHeader = ({
{breadcrumbSegments.length > 0 && (
<span>·</span>
)}
<div ref={containerRef} className="flex-1 flex items-center overflow-hidden mt-0.5">
<div ref={breadcrumbsRef} className="flex items-center overflow-hidden">
<div
ref={containerRef}
className={cn(
"flex-1 min-w-0 flex items-center overflow-hidden mt-0.5",
visibleSegmentCount === null && "invisible",
)}
>
<div ref={breadcrumbsRef} className="flex min-w-0 items-center overflow-hidden">
{hiddenSegments.length > 0 && (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="font-mono text-sm cursor-pointer hover:underline p-1 rounded transition-colors"
className="font-mono text-sm cursor-pointer hover:underline p-1 rounded transition-colors shrink-0"
aria-label="Show hidden path segments"
>
<MoreHorizontal className="h-4 w-4" />
Expand Down Expand Up @@ -285,13 +306,19 @@ export const PathHeader = ({
</>
)}
{visibleSegments.map((segment, index) => (
<div key={segment.fullPath} className="flex items-center">
<div
key={segment.fullPath}
className={cn(
"flex items-center",
index === visibleSegments.length - 1 ? "min-w-0" : "shrink-0",
)}
>
{(isFileIconVisible && index === visibleSegments.length - 1) && (
<VscodeFileIcon fileName={segment.name} className="h-4 w-4 mr-1" />
<VscodeFileIcon fileName={segment.name} className="h-4 w-4 mr-1 flex-shrink-0" />
)}
<Link
className={cn(
"font-mono text-sm truncate cursor-pointer hover:underline",
"font-mono text-sm min-w-0 truncate cursor-pointer hover:underline",
)}
href={getBrowsePath({
repoName: repo.name,
Expand All @@ -311,10 +338,10 @@ export const PathHeader = ({
{breadcrumbSegments.length > 0 && (
<CopyIconButton
onCopy={onCopyPath}
className="ml-2"
className="ml-2 shrink-0"
/>
)}
</div>
</div>
)
}
}
2 changes: 1 addition & 1 deletion packages/web/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export default async function Layout(props: LayoutProps) {
<div className="fixed inset-0 flex bg-shell">
<SidebarProvider defaultOpen={cookieStore.get("sidebar_state")?.value !== "false"}>
{sidebar}
<div className="flex-1 min-h-0 flex flex-col pt-2 pb-2 pr-2 pl-2 md:pl-0">
<div className="flex-1 min-h-0 min-w-0 flex flex-col pt-2 pb-2 pr-2 pl-2 md:pl-0">
<div className="flex-1 min-h-0 bg-background flex flex-col border border-[#e6e6e6] dark:border-[#1d1d1f] rounded-xl overflow-hidden">
<BannerHeightObserver>
<BannerSlot
Expand Down
Loading