Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [EE] Added mermaid diagram rendering to Ask Sourcebot answers, with pan/zoom, copy/export, in-thread deep links, and an interleaved right-panel view. [#1369](https://github.com/sourcebot-dev/sourcebot/pull/1369)
- [EE] Added a context-window usage gauge to the Ask Sourcebot chat details, showing how much of the selected model's context window each turn occupies. Window sizes are resolved from the models.dev catalog. [#1370](https://github.com/sourcebot-dev/sourcebot/pull/1370)
- Added language model input-modality and document capability resolution, automatically resolved from the models.dev catalog (falls back to text-only for uncatalogued/self-hosted models). [#1372](https://github.com/sourcebot-dev/sourcebot/pull/1372)
- Added rendered Markdown previews with a raw source toggle in the code browser. [#1382](https://github.com/sourcebot-dev/sourcebot/pull/1382)

### Fixed
- Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, test, vi } from 'vitest';
import { BlameViewToggle } from './blameViewToggle';

const mocks = vi.hoisted(() => ({
push: vi.fn(),
}));

vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mocks.push,
}),
}));

afterEach(() => {
cleanup();
vi.clearAllMocks();
});

describe('BlameViewToggle', () => {
test('preserves source view when entering blame mode', () => {
render(
<BlameViewToggle
repoName="github.com/sourcebot-dev/sourcebot"
revisionName="main"
path="README.md"
blame={false}
viewMode="source"
/>
);

fireEvent.click(screen.getByRole('radio', { name: 'View blame' }));

expect(mocks.push).toHaveBeenCalledWith(
'/browse/github.com/sourcebot-dev/sourcebot@main/-/blob/README.md?blame=true&view=source'
);
});

test('preserves source view when leaving blame mode', () => {
render(
<BlameViewToggle
repoName="github.com/sourcebot-dev/sourcebot"
revisionName="main"
path="README.md"
blame
viewMode="source"
/>
);

fireEvent.click(screen.getByRole('radio', { name: 'View source code' }));

expect(mocks.push).toHaveBeenCalledWith(
'/browse/github.com/sourcebot-dev/sourcebot@main/-/blob/README.md?view=source'
);
});

test('does not add a view query when rendered preview mode enters blame', () => {
render(
<BlameViewToggle
repoName="github.com/sourcebot-dev/sourcebot"
revisionName="main"
path="README.md"
blame={false}
viewMode="rendered"
/>
);

fireEvent.click(screen.getByRole('radio', { name: 'View blame' }));

expect(mocks.push).toHaveBeenCalledWith(
'/browse/github.com/sourcebot-dev/sourcebot@main/-/blob/README.md?blame=true'
);
});

test('supports markdown-specific non-blame labels', () => {
render(
<BlameViewToggle
repoName="github.com/sourcebot-dev/sourcebot"
revisionName="main"
path="README.md"
blame={false}
viewMode="rendered"
codeLabel="Preview"
codeAriaLabel="Preview rendered markdown"
/>
);

expect(screen.getByRole('radio', { name: 'Preview rendered markdown' }).textContent).toBe('Preview');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@

import { useRouter } from "next/navigation";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { getBrowsePath } from "@/app/(app)/browse/hooks/utils";
import { getBrowsePath, type BlobViewMode } from "@/app/(app)/browse/hooks/utils";

interface BlameViewToggleProps {
repoName: string;
revisionName?: string;
path: string;
blame: boolean;
viewMode?: BlobViewMode;
codeLabel?: string;
codeAriaLabel?: string;
}

export const BlameViewToggle = ({ repoName, revisionName, path, blame }: BlameViewToggleProps) => {
export const BlameViewToggle = ({
repoName,
revisionName,
path,
blame,
viewMode,
codeLabel = 'Code',
codeAriaLabel = 'View source code',
}: BlameViewToggleProps) => {
const router = useRouter();

const handleValueChange = (value: string) => {
Expand All @@ -27,6 +38,7 @@ export const BlameViewToggle = ({ repoName, revisionName, path, blame }: BlameVi
path,
pathType: 'blob',
blame: value === 'blame',
viewMode: viewMode === 'source' ? 'source' : undefined,
}));
};

Expand All @@ -48,10 +60,10 @@ export const BlameViewToggle = ({ repoName, revisionName, path, blame }: BlameVi
>
<ToggleGroupItem
value="code"
aria-label="View source code"
aria-label={codeAriaLabel}
className={`${baseItemClass} rounded-r-none`}
>
Code
{codeLabel}
</ToggleGroupItem>
<ToggleGroupItem
value="blame"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { cn, getCodeHostInfoForRepo, isServiceError, truncateSha } from "@/lib/u
import { X } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { getBrowsePath } from "../../../hooks/utils";
import { type BlobViewMode, getBrowsePath } from "../../../hooks/utils";
import { BlameAgeLegend } from "./blameAgeLegend";
import { BlameViewToggle } from "./blameViewToggle";
import { MarkdownPreviewPanel } from "./markdownPreviewPanel";
import { MarkdownViewToggle } from "./markdownViewToggle";
import { PureCodePreviewPanel } from "./pureCodePreviewPanel";
import { getFileBlame, getFileSource } from '@/features/git';

Expand All @@ -33,9 +35,10 @@ interface CodePreviewPanelProps {
// When true, fetch blame data alongside the file source and pass it to
// the editor so the blame gutter can render.
blame?: boolean;
viewMode?: BlobViewMode;
}

export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRef, blame }: CodePreviewPanelProps) => {
export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRef, blame, viewMode = 'rendered' }: CodePreviewPanelProps) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const contentRef = previewRef ?? revisionName;

const [fileSourceResponse, repoInfoResponse, blameResponse] = await Promise.all([
Expand Down Expand Up @@ -72,6 +75,15 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe
: source.split('\n').length - (source.endsWith('\n') ? 1 : 0);
const byteSize = Buffer.byteLength(source, 'utf-8');
const fileSize = formatFileSize(byteSize);
const isMarkdown = fileSourceResponse.language === 'Markdown';
const isMarkdownPreviewAvailable = isMarkdown && !blame && !previewRef;
const shouldRenderMarkdown = isMarkdownPreviewAvailable && viewMode === 'rendered';
const nonBlameViewLabel = isMarkdown
? viewMode === 'source' ? 'Source' : 'Preview'
: 'Code';
const nonBlameViewAriaLabel = isMarkdown
? viewMode === 'source' ? 'View raw markdown source' : 'Preview rendered markdown'
: 'View source code';

const codeHostInfo = getCodeHostInfoForRepo({
codeHostType: repoInfoResponse.codeHostType,
Expand Down Expand Up @@ -119,11 +131,25 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe
<Separator />
{!previewRef && (
<div className="flex flex-row items-center gap-3 px-4 py-1 border-b shrink-0">
{isMarkdownPreviewAvailable && (
<>
<MarkdownViewToggle
repoName={repoName}
revisionName={revisionName}
path={path}
viewMode={viewMode}
/>
<Separator orientation="vertical" className="h-4" />
</>
)}
<BlameViewToggle
repoName={repoName}
revisionName={revisionName}
path={path}
blame={blame ?? false}
viewMode={viewMode}
codeLabel={nonBlameViewLabel}
codeAriaLabel={nonBlameViewAriaLabel}
/>
<span className="text-sm text-muted-foreground">
{lineCount.toLocaleString()} lines · {fileSize}
Expand Down Expand Up @@ -167,6 +193,7 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe
revisionName,
path,
pathType: 'blob',
viewMode: viewMode === 'source' ? 'source' : undefined,
})}
aria-label="Close preview"
>
Expand All @@ -178,14 +205,24 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe
</Tooltip>
</div>
)}
<PureCodePreviewPanel
source={fileSourceResponse.source}
language={fileSourceResponse.language}
repoName={repoName}
path={path}
revisionName={contentRef ?? 'HEAD'}
blame={blameResponse}
/>
{shouldRenderMarkdown ? (
<MarkdownPreviewPanel
source={fileSourceResponse.source}
repoName={repoName}
revisionName={revisionName}
path={path}
/>
) : (
<PureCodePreviewPanel
source={fileSourceResponse.source}
language={fileSourceResponse.language}
repoName={repoName}
path={path}
revisionName={contentRef ?? 'HEAD'}
viewMode={viewMode}
blame={blameResponse}
/>
)}
</>
)
}
}
Loading