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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- @NOTE: On next release, please bump the MCP pacakge as there are breaking changes in this! -->

### Added
- Implement dynamic tab titles for files and folders in browse tab. [#560](https://github.com/sourcebot-dev/sourcebot/pull/560)

### Fixed
- Fixed "dubious ownership" errors when cloning / fetching repos. [#553](https://github.com/sourcebot-dev/sourcebot/pull/553)

Expand Down
61 changes: 61 additions & 0 deletions packages/web/src/app/[domain]/browse/[...path]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,67 @@ import { getBrowseParamsFromPathParam } from "../hooks/utils";
import { CodePreviewPanel } from "./components/codePreviewPanel";
import { Loader2 } from "lucide-react";
import { TreePreviewPanel } from "./components/treePreviewPanel";
import { Metadata } from "next";

/**
* 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 A formatted title string.
*/
const parsePathForTitle = (path: string[]): string => {
const pathParam = path.join('/');

const { repoName, revisionName, path: filePath, pathType } = getBrowseParamsFromPathParam(pathParam);

// Build the base repository and revision string.
const cleanRepoName = repoName.split('/').slice(1).join('/'); // Remove the version control system prefix
const repoAndRevision = `${cleanRepoName}${revisionName ? ` @ ${revisionName}` : ''}`;

switch (pathType) {
case 'blob': {
// For blobs, get the filename from the end of the path.
const fileName = filePath.split('/').pop() || filePath;
return `${fileName} - ${repoAndRevision}`;
}
case 'tree': {
// If the path is empty, it's the repo root.
if (filePath === '' || filePath === '/') {
return repoAndRevision;
}
// Otherwise, show the directory path.
const directoryPath = filePath.endsWith('/') ? filePath : `${filePath}/`;
return `${directoryPath} - ${repoAndRevision}`;
}
}
}

type Props = {
params: Promise<{
domain: string;
path: string[];
}>;
};

export async function generateMetadata({ params: paramsPromise }: Props): Promise<Metadata> {
let title = 'Browse'; // Default Fallback

try {
const params = await paramsPromise;
title = parsePathForTitle(params.path);

} catch (error) {
console.error("Failed to generate metadata title from path:", error);
}

return {
title,
};
}

interface BrowsePageProps {
params: Promise<{
Expand Down
12 changes: 9 additions & 3 deletions packages/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ import { PlanProvider } from "@/features/entitlements/planProvider";
import { getEntitlements } from "@sourcebot/shared";

export const metadata: Metadata = {
title: "Sourcebot",
description: "Sourcebot is a self-hosted code understanding tool. Ask questions about your codebase and get rich Markdown answers with inline citations.",
manifest: "/manifest.json",
// Using the title.template will allow child pages to set the title
// while keeping a consistent suffix.
title: {
default: "Sourcebot",
template: "%s | Sourcebot",
},
description:
"Sourcebot is a self-hosted code understanding tool. Ask questions about your codebase and get rich Markdown answers with inline citations.",
manifest: "/manifest.json",
};

export default function RootLayout({
Expand Down
Loading