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

## [Unreleased]

### Changed
- Changed the me-control to render the user's avatar in the top-bar. [#874](https://github.com/sourcebot-dev/sourcebot/pull/874)
- Moved the "current version" indicator into the "what's new" dropdown. [#874](https://github.com/sourcebot-dev/sourcebot/pull/874)

### Removed
- Removed the Discord and GitHub buttons from the top corner. [#874](https://github.com/sourcebot-dev/sourcebot/pull/874)

## [4.10.29] - 2026-02-10

### Changed
Expand Down
71 changes: 8 additions & 63 deletions packages/web/src/app/[domain]/browse/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,17 @@
'use client';

import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { BottomPanel } from "./components/bottomPanel";
import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle";
import { BrowseStateProvider } from "./browseStateProvider";
import { FileTreePanel } from "./components/fileTreePanel";
import { TopBar } from "@/app/[domain]/components/topBar";
import { useBrowseParams } from "./hooks/useBrowseParams";
import { FileSearchCommandDialog } from "./components/fileSearchCommandDialog";
import { useDomain } from "@/hooks/useDomain";
import { SearchBar } from "../components/searchBar";
import escapeStringRegexp from "escape-string-regexp";
import { auth } from "@/auth";
import { LayoutClient } from "./layoutClient";

interface LayoutProps {
children: React.ReactNode;
}

export default function Layout({
export default async function Layout({
children,
}: LayoutProps) {
const { repoName, revisionName } = useBrowseParams();
const domain = useDomain();

const session = await auth();
return (
<BrowseStateProvider>
<div className="flex flex-col h-screen">
<TopBar
domain={domain}
>
<SearchBar
size="sm"
defaults={{
query: `repo:^${escapeStringRegexp(repoName)}$${revisionName ? ` rev:${revisionName}` : ''} `,
}}
className="w-full"
/>
</TopBar>
<ResizablePanelGroup
direction="horizontal"
>
<FileTreePanel order={1} />

<AnimatedResizableHandle />

<ResizablePanel
order={2}
minSize={10}
defaultSize={80}
id="code-preview-panel-container"
>
<ResizablePanelGroup
direction="vertical"
>
<ResizablePanel
order={1}
id="code-preview-panel"
>
{children}
</ResizablePanel>
<AnimatedResizableHandle />
<BottomPanel
order={2}
/>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</div>
<FileSearchCommandDialog />
</BrowseStateProvider>
);
<LayoutClient session={session}>
{children}
</LayoutClient>
)
}
76 changes: 76 additions & 0 deletions packages/web/src/app/[domain]/browse/layoutClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client';

import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { BottomPanel } from "./components/bottomPanel";
import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle";
import { BrowseStateProvider } from "./browseStateProvider";
import { FileTreePanel } from "./components/fileTreePanel";
import { TopBar } from "@/app/[domain]/components/topBar";
import { useBrowseParams } from "./hooks/useBrowseParams";
import { FileSearchCommandDialog } from "./components/fileSearchCommandDialog";
import { useDomain } from "@/hooks/useDomain";
import { SearchBar } from "../components/searchBar";
import escapeStringRegexp from "escape-string-regexp";
import { Session } from "next-auth";

interface LayoutProps {
children: React.ReactNode;
session: Session | null;
}

export function LayoutClient({
children,
session,
}: LayoutProps) {
const { repoName, revisionName } = useBrowseParams();
const domain = useDomain();

return (
<BrowseStateProvider>
<div className="flex flex-col h-screen">
<TopBar
domain={domain}
session={session}
>
<SearchBar
size="sm"
defaults={{
query: `repo:^${escapeStringRegexp(repoName)}$${revisionName ? ` rev:${revisionName}` : ''} `,
}}
className="w-full"
/>
</TopBar>
<ResizablePanelGroup
direction="horizontal"
>
<FileTreePanel order={1} />

<AnimatedResizableHandle />

<ResizablePanel
order={2}
minSize={10}
defaultSize={80}
id="code-preview-panel-container"
>
<ResizablePanelGroup
direction="vertical"
>
<ResizablePanel
order={1}
id="code-preview-panel"
>
{children}
</ResizablePanel>
<AnimatedResizableHandle />
<BottomPanel
order={2}
/>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</div>
<FileSearchCommandDialog />
</BrowseStateProvider>
);
}
1 change: 1 addition & 0 deletions packages/web/src/app/[domain]/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default async function Page(props: PageProps) {
<TopBar
domain={params.domain}
homePath={`/${params.domain}/chat`}
session={session}
>
<div className="flex flex-row gap-2 items-center">
<span className="text-muted mx-2 select-none">/</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Settings2Icon } from "lucide-react"
import { AppearanceDropdownMenuGroup } from "./appearanceDropdownMenuGroup"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"

interface AppearanceDropdownMenuProps {
className?: string;
}

export const AppearanceDropdownMenu = ({ className }: AppearanceDropdownMenuProps) => {
return (
<DropdownMenu>
<Tooltip
delayDuration={100}
>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className={className}>
<Settings2Icon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>
Appearance settings
</TooltipContent>
</Tooltip>
<DropdownMenuContent>
<AppearanceDropdownMenuGroup />
</DropdownMenuContent>
</DropdownMenu>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use client';

import {
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger
} from "@/components/ui/dropdown-menu"
import { useKeymapType } from "@/hooks/useKeymapType"
import { KeymapType } from "@/lib/types"
import {
CodeIcon,
Laptop,
Moon,
Sun
} from "lucide-react"
import { useTheme } from "next-themes"
import { useMemo } from "react"

export const AppearanceDropdownMenuGroup = () => {
const { theme: _theme, setTheme } = useTheme();
const [keymapType, setKeymapType] = useKeymapType();

const theme = useMemo(() => {
return _theme ?? "light";
}, [_theme]);

const ThemeIcon = useMemo(() => {
switch (theme) {
case "light":
return <Sun className="h-4 w-4 mr-2" />;
case "dark":
return <Moon className="h-4 w-4 mr-2" />;
case "system":
return <Laptop className="h-4 w-4 mr-2" />;
default:
return <Laptop className="h-4 w-4 mr-2" />;
}
}, [theme]);

return (
<DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
{ThemeIcon}
<span>Theme</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
<DropdownMenuRadioItem value="light">
Light
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">
Dark
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">
System
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<CodeIcon className="h-4 w-4 mr-2" />
<span>Code navigation</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={keymapType} onValueChange={(value) => setKeymapType(value as KeymapType)}>
<DropdownMenuRadioItem value="default">
Default
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="vim">
Vim
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
)
}
90 changes: 90 additions & 0 deletions packages/web/src/app/[domain]/components/meControlDropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use client';

import {
LogOut,
Settings,
} from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { signOut } from "next-auth/react"
import posthog from "posthog-js";
import { useDomain } from "@/hooks/useDomain";
import { Session } from "next-auth";
import { AppearanceDropdownMenuGroup } from "./appearanceDropdownMenuGroup";
import placeholderAvatar from "@/public/placeholder_avatar.png";

interface MeControlDropdownMenuProps {
menuButtonClassName?: string;
session: Session;
}

export const MeControlDropdownMenu = ({
menuButtonClassName,
session,
}: MeControlDropdownMenuProps) => {
const domain = useDomain();

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className={cn("h-8 w-8 cursor-pointer", menuButtonClassName)}>
<AvatarImage src={session.user.image ?? placeholderAvatar.src} />
<AvatarFallback className="bg-primary/10 text-primary font-semibold text-sm">
{session.user.name && session.user.name.length > 0 ? session.user.name[0].toUpperCase() : 'U'}
</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="end" sideOffset={5}>
<DropdownMenuGroup>
<div className="flex flex-row items-center gap-3 px-3 py-3">
<Avatar className="h-10 w-10 flex-shrink-0">
<AvatarImage
src={session.user.image ?? ""}
/>
<AvatarFallback className="bg-primary/10 text-primary font-semibold">
{session.user.name && session.user.name.length > 0 ? session.user.name[0].toUpperCase() : 'U'}
</AvatarFallback>
</Avatar>
<div className="flex flex-col flex-1 min-w-0">
<p className="text-sm font-semibold truncate">{session.user.name ?? "User"}</p>
{session.user.email && (
<p className="text-xs text-muted-foreground truncate">{session.user.email}</p>
)}
</div>
</div>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<AppearanceDropdownMenuGroup />
<DropdownMenuItem asChild>
<a href={`/${domain}/settings`}>
<Settings className="h-4 w-4 mr-2" />
<span>Settings</span>
</a>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => {
signOut({
redirectTo: "/login",
}).then(() => {
posthog.reset();
})
}}
>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
Loading