SIDEBAR_CHAT_LIMIT}
@@ -64,6 +72,40 @@ export async function DefaultSidebar() {
);
}
+const getRecentRepoVisits = async () => sew(() =>
+ withAuth(async ({ org, user, prisma }) => {
+ const visits = await prisma.repoVisit.findMany({
+ where: {
+ userId: user.id,
+ orgId: org.id,
+ },
+ orderBy: {
+ lastPromotedAt: 'desc',
+ },
+ take: SIDEBAR_REPO_VISITS_LIMIT,
+ include: {
+ repo: {
+ select: {
+ id: true,
+ name: true,
+ displayName: true,
+ imageUrl: true,
+ external_codeHostType: true,
+ },
+ },
+ },
+ });
+
+ return visits.map((visit) => ({
+ repoId: visit.repo.id,
+ repoName: visit.repo.name,
+ displayName: visit.repo.displayName,
+ imageUrl: visit.repo.imageUrl,
+ codeHostType: visit.repo.external_codeHostType,
+ }));
+ })
+);
+
const getUserChatHistory = async () => sew(() =>
withAuth(async ({ org, user, prisma }) => {
const chats = await prisma.chat.findMany({
diff --git a/packages/web/src/app/(app)/@sidebar/components/defaultSidebar/repoVisitHistory.tsx b/packages/web/src/app/(app)/@sidebar/components/defaultSidebar/repoVisitHistory.tsx
new file mode 100644
index 000000000..72ee9886b
--- /dev/null
+++ b/packages/web/src/app/(app)/@sidebar/components/defaultSidebar/repoVisitHistory.tsx
@@ -0,0 +1,92 @@
+'use client';
+
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar";
+import { getCodeHostIcon, getRepoImageSrc } from "@/lib/utils";
+import { CodeHostType } from "@prisma/client";
+import Image from "next/image";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { cn } from "@/lib/utils";
+
+export interface RepoVisitItem {
+ repoId: number;
+ repoName: string;
+ displayName: string | null;
+ imageUrl: string | null;
+ codeHostType: CodeHostType;
+}
+
+interface RepoVisitHistoryProps {
+ repoVisits: RepoVisitItem[];
+}
+
+export function RepoVisitHistory({ repoVisits }: RepoVisitHistoryProps) {
+ const pathname = usePathname();
+
+ if (repoVisits.length === 0) {
+ return null;
+ }
+
+ return (
+
+ Recent Repositories
+
+
+ {repoVisits.map((visit) => {
+ const href = `/browse/${visit.repoName}/-/tree/`;
+ const browsePrefix = `/browse/${visit.repoName}`;
+ const isActive = pathname === browsePrefix
+ || pathname.startsWith(`${browsePrefix}/-/`)
+ || pathname.startsWith(`${browsePrefix}@`);
+ const repoImageSrc = visit.imageUrl
+ ? getRepoImageSrc(visit.imageUrl, visit.repoId)
+ : undefined;
+ const codeHostIcon = getCodeHostIcon(visit.codeHostType);
+ const isInternalApiImage = repoImageSrc?.startsWith('/api/');
+
+ const name = visit.displayName ?
+ visit.displayName.split('/').pop() :
+ visit.repoName;
+
+ return (
+
+
+
+ {repoImageSrc ? (
+
+ ) : (
+
+ )}
+
+ {name}
+
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/packages/web/src/app/(app)/browse/[...path]/components/trackRepoVisit.tsx b/packages/web/src/app/(app)/browse/[...path]/components/trackRepoVisit.tsx
new file mode 100644
index 000000000..e7f40dea8
--- /dev/null
+++ b/packages/web/src/app/(app)/browse/[...path]/components/trackRepoVisit.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import { trackRepoVisit } from "@/app/(app)/browse/actions";
+import { isServiceError } from "@/lib/utils";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+interface TrackRepoVisitProps {
+ repoName: string;
+ isAuthenticated: boolean;
+}
+
+export function TrackRepoVisit({ repoName, isAuthenticated }: TrackRepoVisitProps) {
+ const router = useRouter();
+ useEffect(() => {
+ if (!isAuthenticated) {
+ return;
+ }
+
+ trackRepoVisit({ repoName }).then((result) => {
+ if (!isServiceError(result) && result.wasPromoted) {
+ router.refresh();
+ }
+ });
+ }, [repoName, router, isAuthenticated]);
+
+ return null;
+}
diff --git a/packages/web/src/app/(app)/browse/[...path]/page.tsx b/packages/web/src/app/(app)/browse/[...path]/page.tsx
index 7680ada7f..a051e5bb2 100644
--- a/packages/web/src/app/(app)/browse/[...path]/page.tsx
+++ b/packages/web/src/app/(app)/browse/[...path]/page.tsx
@@ -7,6 +7,8 @@ import { CommitsPanel } from "./components/commitHistoryPanel/commitsPanel";
import { Loader2 } from "lucide-react";
import { TreePreviewPanel } from "./components/treePreviewPanel/treePreviewPanel";
import { Metadata } from "next";
+import { TrackRepoVisit } from "./components/trackRepoVisit";
+import { auth } from "@/auth";
/**
* Parses the URL path to generate a descriptive title.
@@ -94,7 +96,7 @@ interface BrowsePageProps {
}
export default async function BrowsePage(props: BrowsePageProps) {
- const [params, searchParams] = await Promise.all([props.params, props.searchParams]);
+ const [params, searchParams, session] = await Promise.all([props.params, props.searchParams, auth()]);
const {
path: _rawPath,
@@ -114,6 +116,7 @@ export default async function BrowsePage(props: BrowsePageProps) {
return (
+
diff --git a/packages/web/src/app/(app)/browse/actions.ts b/packages/web/src/app/(app)/browse/actions.ts
new file mode 100644
index 000000000..47bd13d55
--- /dev/null
+++ b/packages/web/src/app/(app)/browse/actions.ts
@@ -0,0 +1,60 @@
+'use server';
+
+import { sew } from "@/middleware/sew";
+import { withAuth } from "@/middleware/withAuth";
+import { SIDEBAR_REPO_VISITS_LIMIT } from "../@sidebar/components/defaultSidebar";
+
+export const trackRepoVisit = async ({ repoName }: { repoName: string }) => sew(() =>
+ withAuth(async ({ org, user, prisma }) => {
+ const repo = await prisma.repo.findFirst({
+ where: {
+ name: repoName,
+ orgId: org.id,
+ },
+ select: { id: true },
+ });
+
+ if (!repo) {
+ return {
+ wasPromoted: false,
+ }
+ }
+
+ const topKVisits = await prisma.repoVisit.findMany({
+ where: {
+ userId: user.id,
+ orgId: org.id
+ },
+ orderBy: { lastPromotedAt: 'desc'},
+ take: SIDEBAR_REPO_VISITS_LIMIT,
+ select: { repoId: true }
+ });
+
+ const shouldPromote = !topKVisits.some((visit) => visit.repoId === repo.id);
+ const now = new Date();
+
+ await prisma.repoVisit.upsert({
+ where: {
+ repoId_userId: {
+ repoId: repo.id,
+ userId: user.id,
+ },
+ },
+ update: {
+ visitedAt: now,
+ ...(shouldPromote ? {
+ lastPromotedAt: now,
+ } : {})
+ },
+ create: {
+ repoId: repo.id,
+ userId: user.id,
+ orgId: org.id,
+ },
+ });
+
+ return {
+ wasPromoted: shouldPromote,
+ };
+ })
+);