diff --git a/packages/web/src/app/[domain]/browse/[...path]/page.tsx b/packages/web/src/app/[domain]/browse/[...path]/page.tsx index bf16cc6a..83ab5d2d 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/page.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/page.tsx @@ -4,7 +4,7 @@ import { CodePreviewPanel } from "./components/codePreviewPanel"; import { Loader2 } from "lucide-react"; import { TreePreviewPanel } from "./components/treePreviewPanel"; import { Metadata } from "next"; -import { parseRepoPath } from "@/lib/utils"; +import { parsePathForTitle} from "@/lib/utils"; type Props = { params: { @@ -14,26 +14,22 @@ type Props = { }; export async function generateMetadata({ params }: Props): Promise { - let title = 'Browse'; // Current Default + let title = 'Browse'; // Default Fallback try { - const parsedInfo = parseRepoPath(params.path); + title = parsePathForTitle(params.path); - if (parsedInfo) { - const { fullRepoName, revision } = parsedInfo; - title = `${fullRepoName}${revision ? ` @ ${revision}` : ''}`; - } } catch (error) { - // Log the error for debugging, but don't crash the page render. + // TODO: Maybe I need to look into a better way of handling this error. + // for now, it is just a log, fallback tab title and prevents the app from crashing. console.error("Failed to generate metadata title from path:", params.path, error); } return { - title, // e.g., "sourcebot-dev/sourcebot @ HEAD" + title, }; } - interface BrowsePageProps { params: Promise<{ path: string[]; @@ -48,6 +44,7 @@ export default async function BrowsePage(props: BrowsePageProps) { } = params; const rawPath = _rawPath.join('/'); + console.log("rawPath:", rawPath); const { repoName, revisionName, path, pathType } = getBrowseParamsFromPathParam(rawPath); return ( diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts index e13077bc..2afe8ef6 100644 --- a/packages/web/src/lib/utils.ts +++ b/packages/web/src/lib/utils.ts @@ -488,54 +488,61 @@ export const isHttpError = (error: unknown, status: number): boolean => { && error.status === status; } - /** - * Parses a URL path array to extract the full repository name and revision. - * This function assumes a URL structure like: - * `.../[hostname]/[owner]/[repo@revision]/-/tree/...` - * Or for nested groups (like GitLab): - * `.../[hostname]/[group]/[subgroup]/[repo@revision]/-/tree/...` + * Parses the URL path to generate a descriptive title. + * It handles three cases: + * 1. File view (`blob`): "filename.ts - owner/repo" + * 2. Directory view (`tree`): "directory/ - owner/repo" + * 3. Repository root: "owner/repo" * * @param path The array of path segments from Next.js params. - * @returns An object with fullRepoName and revision, or null if parsing fails. + * @returns A formatted title string. */ -export const parseRepoPath = (path: string[]): { fullRepoName: string; revision: string } | null => { - if (path.length < 2) { - return null; // Not enough path segments to parse. - } - - // Find the index of the `-` delimiter which separates the repo info from the file tree info. +export const parsePathForTitle = (path: string[]): string => { const delimiterIndex = path.indexOf('-'); - - // If no delimiter is found, we can't reliably parse the path. - if (delimiterIndex === -1) { - return null; + if (delimiterIndex === -1 || delimiterIndex === 0) { + return 'Browse'; } - // The repository parts are between the hostname (index 0) and the delimiter. - // e.g., ["github.com", "sourcebot-dev", "sourcebot"] -> slice will be ["sourcebot-dev", "sourcebot"] const repoParts = path.slice(1, delimiterIndex); + if (repoParts.length === 0) return 'Browse'; - if (repoParts.length === 0) { - return null; + const lastPart = decodeURIComponent(repoParts.pop()!); + const [repoNamePart, revision = ''] = lastPart.split('@'); + const ownerParts = repoParts; + const fullRepoName = [...ownerParts, repoNamePart].join('/'); + const repoAndRevision = `${fullRepoName}${revision ? ` @ ${revision}` : ''}`; + + // Check for file (`blob`) or directory (`tree`) view + const blobIndex = path.indexOf('blob'); + const treeIndex = path.indexOf('tree'); + + // Case 1: Viewing a file + if (blobIndex !== -1 && path.length > blobIndex + 1) { + const encodedFilePath = path[blobIndex + 1]; + const filePath = decodeURIComponent(encodedFilePath); + + const fileName = filePath.split('/').pop() || filePath; + + // Return a title like: "agents.ts - sourcebot-dev/sourcebot @ HEAD" + return `${fileName} - ${repoAndRevision}`; } - // The last part of the repo segment potentially contains the revision. - const lastPart = repoParts[repoParts.length - 1]; + // Case 2: Viewing a directory + if (treeIndex !== -1 && path.length > treeIndex + 1) { + const encodedDirPath = path[treeIndex + 1]; + const dirPath = decodeURIComponent(encodedDirPath); + + // If we're at the root of the tree, just show the repo name + if (dirPath === '/' || dirPath === '') { + return repoAndRevision; + } - // URL segments are encoded. Decode it to handle characters like '@' (%40). - const decodedLastPart = decodeURIComponent(lastPart); + // Otherwise, show the directory path + // Return a title like: "client/src/store/ - sourcebot-dev/sourcebot @ HEAD" + return `${dirPath.endsWith('/') ? dirPath : dirPath + '/'} - ${repoAndRevision}`; + } - const [repoNamePart, revision = ''] = decodedLastPart.split('@'); - - // The preceding parts form the owner/group path. - // e.g., ["sourcebot"] or ["my-group", "my-subgroup"] - const ownerParts = repoParts.slice(0, repoParts.length - 1); - - // Reconstruct the full repository name. - // e.g., "sourcebot-dev" + "/" + "sourcebot" - // e.g., "my-group/my-subgroup" + "/" + "my-repo" - const fullRepoName = [...ownerParts, repoNamePart].join('/'); - - return { fullRepoName, revision }; + // Case 3: Fallback to the repository root + return repoAndRevision; } \ No newline at end of file