Skip to content

feat: add markdown preview with toggle to view raw source#863

Draft
Naresh-official wants to merge 3 commits intosourcebot-dev:mainfrom
Naresh-official:feature/markdown-preview-toggle
Draft

feat: add markdown preview with toggle to view raw source#863
Naresh-official wants to merge 3 commits intosourcebot-dev:mainfrom
Naresh-official:feature/markdown-preview-toggle

Conversation

@Naresh-official
Copy link

@Naresh-official Naresh-official commented Feb 7, 2026

Fix: Add toggle for Markdown preview #794

Summary

This PR adds a toggle to control the visibility of the Markdown preview panel. The preview is now rendered conditionally based on user intent.

Details

  • Introduces a UI toggle to enable or disable the Markdown preview
  • Preview component is only mounted when enabled

Results

Screenshot from 2026-02-07 16-28-39 Screenshot from 2026-02-07 16-29-11

Summary by CodeRabbit

  • New Features
    • Added markdown file preview with toggle to switch between code and rendered views
    • Enhanced markdown display with syntax highlighting, inline image support, embedded video rendering, styled tables, and GitHub-flavored markdown formatting

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 7, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

This PR adds markdown preview functionality to the code preview panel, introduces new toggle-group UI components from Radix UI, and updates dependencies. A new client-side component enables users to toggle between code and markdown views, with dedicated markdown rendering utilizing ReactMarkdown and syntax highlighting.

Changes

Cohort / File(s) Summary
Dependency Updates
packages/web/package.json
Bumped @radix-ui/react-toggle to ^1.1.10, added @radix-ui/react-toggle-group ^1.1.11, and added github-markdown-css and rehype-highlight for markdown rendering.
UI Components
packages/web/src/components/ui/toggle-group.tsx
New ToggleGroup and ToggleGroupItem components wrapping Radix UI's toggle group, with internal context for propagating variant and size styling.
Code Preview Refactoring
packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx
Replaced PureCodePreviewPanel with CodePreviewPanelClient and added path and revisionName props to component invocation.
Markdown Preview Panel
packages/web/src/app/[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx
New markdown rendering component with ReactMarkdown, custom component mappings for code blocks, images, videos, and tables, plus GitHub Markdown and syntax highlighting CSS.
Code Preview Client Component
packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanelClient.tsx
New client component managing view mode state ("preview" or "code"), detecting markdown files, and conditionally rendering PureMarkDownPreviewPanel or PureCodePreviewPanel.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CodePreviewPanelClient
    participant ToggleGroup
    participant ViewState as View Mode State
    participant MarkdownPanel as PureMarkDownPreviewPanel
    participant CodePanel as PureCodePreviewPanel

    User->>CodePreviewPanelClient: Open file (markdown detected)
    CodePreviewPanelClient->>ViewState: Initialize viewMode = "preview"
    CodePreviewPanelClient->>ToggleGroup: Render toggle buttons (Preview/Code)
    CodePreviewPanelClient->>MarkdownPanel: Render markdown preview

    User->>ToggleGroup: Click "Code" button
    ToggleGroup->>ViewState: Update viewMode = "code"
    CodePreviewPanelClient->>CodePanel: Render code view
    CodePanel-->>User: Display syntax highlighted code

    User->>ToggleGroup: Click "Preview" button
    ToggleGroup->>ViewState: Update viewMode = "preview"
    CodePreviewPanelClient->>MarkdownPanel: Render markdown preview
    MarkdownPanel->>MarkdownPanel: Process markdown with custom renderers
    MarkdownPanel-->>User: Display formatted markdown
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Suggested reviewers

  • brendan-kellam
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding markdown preview functionality with a toggle to view raw source code.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In
`@packages/web/src/app/`[domain]/browse/[...path]/components/codePreviewPanelClient.tsx:
- Line 26: Remove the debugging console.log in the CodePreviewPanelClient
component: delete the line that calls console.log({language, path, repoName,
revisionName}) (or replace it with a dev-only guard using
NODE_ENV==='development' if you want to keep local debugging). Locate the
console.log call inside the CodePreviewPanelClient component in
codePreviewPanelClient.tsx and remove or guard it so it does not run in
production.

In
`@packages/web/src/app/`[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx:
- Around line 86-99: The video renderer in pureMarkDownPreviewPanel.tsx (the
video: ({ src = "", ...props }) => { ... } handler) currently passes src through
unchanged, so relative video paths break; update this handler to rewrite
relative src the same way the img and source handlers do (use the same
IMAGE_BASE_URL or the existing resolve logic used for img/source) before
rendering the <video>, ensuring the src is resolved for both absolute and
relative URLs while preserving {...props} and controls.
- Line 22: IMAGE_BASE_URL is currently hardcoded to GitHub's raw domain which
breaks image resolution for non-GitHub repos; update
pureMarkDownPreviewPanel.tsx to build the image base URL from the repository's
code host instead of assuming raw.githubusercontent.com: either accept a
precomputed imageBaseUrl prop from the server component (which knows the code
host) or add code-host-aware logic that inspects repoName/revisionName and
composes the correct raw file URL for GitHub, GitLab, Bitbucket, and ADO; change
the constant IMAGE_BASE_URL to use the new prop or host-aware builder and adjust
any references in the component to use that value.
- Around line 25-26: In pureMarkDownPreviewPanel.tsx replace the hardcoded
bg-white on the wrapper with bg-background and remove the unconditional "dark"
class on the <article>; instead wire the component to the app theme (e.g., use
next-themes or the existing theme context) and conditionally add the "dark"
class to the article only when the current theme is dark so github-markdown-css
toggles correctly, and ensure you use Tailwind tokens like
bg-background/border-border/text-foreground rather than CSS variable syntax;
update the JSX in the PureMarkDownPreviewPanel component to read the theme and
apply classes accordingly.
- Around line 27-29: The ReactMarkdown rendering currently uses rehypeRaw
without sanitization (see ReactMarkdown component and the rehypePlugins array
containing rehypeRaw and rehypeHighlight); import and add rehypeSanitize to the
rehypePlugins list immediately after rehypeRaw so raw HTML is sanitized before
rendering, and if your UI requires additional tags/attributes (e.g., video,
source, iframe attrs), extend rehypeSanitize's schema (starting from
defaultSchema) to allowlist those elements/attributes before passing it to
rehypeSanitize.
🧹 Nitpick comments (4)
packages/web/src/app/[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx (2)

150-160: Hardcoded text-blue-600 doesn't respect the app's theme.

Use a semantic Tailwind color class (e.g., text-primary or text-foreground) instead.

As per coding guidelines, "Use Tailwind color classes directly (e.g., border-border, bg-card, text-foreground) instead of CSS variable syntax (e.g., border-[var(--border)])".


42-49: The .github/ prefix check is redundant.

srcSet.startsWith(".github/") is already covered by !srcSet.startsWith("http"). The same redundancy exists in the img handler (Line 65). Consider simplifying:

Simplified condition
-                                if (
-                                    srcSet.startsWith(".github/") ||
-                                    !srcSet.startsWith("http")
-                                ) {
+                                if (!srcSet.startsWith("http")) {
packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanelClient.tsx (2)

32-35: defaultValue is redundant in a controlled ToggleGroup.

Since you're passing value={viewMode}, the component is controlled and defaultValue is ignored by Radix. Remove it to avoid confusion.

Fix
                        <ToggleGroup
                            type="single"
-                            defaultValue="preview"
                            value={viewMode}
                            onValueChange={(value) => value && setViewMode(value)}
                        >

23-24: Improve isMarkdown detection readability and type safety.

  1. The "gcc machine description" check is a necessary workaround — linguist-languages sometimes misidentifies .md files with this language. Add a comment explaining this.
  2. The viewMode state should use a union type ("preview" | "code") instead of string for type safety.
  3. The long boolean expression is hard to scan and should be refactored for clarity.
Suggested refactor
-    const [viewMode, setViewMode] = useState<string>("preview");
-    const isMarkdown = language.toLowerCase() === "gcc machine description" || language.toLowerCase() === "md" || path.toLocaleLowerCase().endsWith(".md") || path.toLocaleLowerCase().endsWith(".markdown");
+    const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
+    const lang = language.toLowerCase();
+    const lowerPath = path.toLowerCase();
+    // "gcc machine description" is included because some language classifiers
+    // misidentify .md files as GCC Machine Description files.
+    const isMarkdown =
+        lang === "markdown" ||
+        lang === "gcc machine description" ||
+        lowerPath.endsWith(".md") ||
+        lowerPath.endsWith(".markdown");

Note: linguist-languages returns "Markdown" as the canonical language name for .md files, not "md", so the language === "md" check may be redundant. Use "markdown" to match the library's actual output.

const [viewMode, setViewMode] = useState<string>("preview");
const isMarkdown = language.toLowerCase() === "gcc machine description" || language.toLowerCase() === "md" || path.toLocaleLowerCase().endsWith(".md") || path.toLocaleLowerCase().endsWith(".markdown");

console.log({language,path,repoName,revisionName});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove console.log before merging.

This is a debugging artifact that will log to the browser console on every render in production.

Fix
-    console.log({language,path,repoName,revisionName});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log({language,path,repoName,revisionName});
🤖 Prompt for AI Agents
In
`@packages/web/src/app/`[domain]/browse/[...path]/components/codePreviewPanelClient.tsx
at line 26, Remove the debugging console.log in the CodePreviewPanelClient
component: delete the line that calls console.log({language, path, repoName,
revisionName}) (or replace it with a dev-only guard using
NODE_ENV==='development' if you want to keep local debugging). Locate the
console.log call inside the CodePreviewPanelClient component in
codePreviewPanelClient.tsx and remove or guard it so it does not run in
production.

repoName,
revisionName,
}: PureMarkDownPreviewPanelProps) => {
const IMAGE_BASE_URL = "https://raw.githubusercontent.com/"+repoName.split("/").slice(1).join("/")+"/"+revisionName+"/";
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

IMAGE_BASE_URL is hardcoded to GitHub's raw.githubusercontent.com.

This component is used in a codebase that supports multiple code hosts (GitHub, GitLab, Bitbucket, Azure DevOps — as evidenced by ADO handling in codePreviewPanel.tsx Line 42). Hardcoding the URL to GitHub means image resolution will break for all non-GitHub repositories.

Consider either:

  1. Passing an image base URL from the server component (which has access to code host info).
  2. Making the URL construction code-host-aware.
🤖 Prompt for AI Agents
In
`@packages/web/src/app/`[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx
at line 22, IMAGE_BASE_URL is currently hardcoded to GitHub's raw domain which
breaks image resolution for non-GitHub repos; update
pureMarkDownPreviewPanel.tsx to build the image base URL from the repository's
code host instead of assuming raw.githubusercontent.com: either accept a
precomputed imageBaseUrl prop from the server component (which knows the code
host) or add code-host-aware logic that inspects repoName/revisionName and
composes the correct raw file URL for GitHub, GitLab, Bitbucket, and ADO; change
the constant IMAGE_BASE_URL to use the new prop or host-aware builder and adjust
any references in the component to use that value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a more complete solution may be to serve up assets from our own apis instead of having to hit the code host. That is a larger change however, so I'm not opposed with having something like what you have now as a MVP solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brendan-kellam Thanks for the context! That makes sense—serving assets through your own APIs would indeed be a cleaner and more maintainable solution that works uniformly across all code hosts.

I understand you're okay with the current GitHub-hardcoded approach as an MVP to get the feature out. Would you like me to create a follow-up issue to track the future work of serving markdown assets (images, videos, etc.) through Sourcebot's own APIs instead of direct code-host URLs? That way the technical debt is documented and can be prioritized later.


✏️ Learnings added
Learnt from: brendan-kellam
Repo: sourcebot-dev/sourcebot PR: 863
File: packages/web/src/app/[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx:22-22
Timestamp: 2026-02-08T00:36:03.683Z
Learning: In the Sourcebot codebase, the preferred architecture for serving markdown assets (images, videos, etc.) in preview panels is to serve them through Sourcebot's own APIs rather than constructing direct URLs to code hosts like raw.githubusercontent.com. This provides a uniform solution across all supported code hosts (GitHub, GitLab, Bitbucket, Azure DevOps).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +25 to +26
<div className="w-full flex justify-center bg-white dark:bg-background">
<article className="markdown-body dark dark:bg-background w-full max-w-4xl px-6 py-10">
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded bg-white and unconditional dark class cause theme issues.

  • bg-white doesn't respect the app's theme system. Use bg-background for both modes.
  • The dark class on the <article> is applied unconditionally, which will cause github-markdown-css to always render dark-mode styles, even when the app is in light mode.
Proposed fix
-            <div className="w-full flex justify-center bg-white dark:bg-background">
-                <article className="markdown-body dark dark:bg-background w-full max-w-4xl px-6 py-10">
+            <div className="w-full flex justify-center bg-background">
+                <article className="markdown-body w-full max-w-4xl px-6 py-10">

You'll need to integrate with the app's theme (e.g., via next-themes) to conditionally apply github-markdown-css's dark/light mode class.

As per coding guidelines, "Use Tailwind color classes directly (e.g., border-border, bg-card, text-foreground) instead of CSS variable syntax (e.g., border-[var(--border)])".

🤖 Prompt for AI Agents
In
`@packages/web/src/app/`[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx
around lines 25 - 26, In pureMarkDownPreviewPanel.tsx replace the hardcoded
bg-white on the wrapper with bg-background and remove the unconditional "dark"
class on the <article>; instead wire the component to the app theme (e.g., use
next-themes or the existing theme context) and conditionally add the "dark"
class to the article only when the current theme is dark so github-markdown-css
toggles correctly, and ensure you use Tailwind tokens like
bg-background/border-border/text-foreground rather than CSS variable syntax;
update the JSX in the PureMarkDownPreviewPanel component to read the theme and
apply classes accordingly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image +1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brendan-kellam The Comment image shows the CHANGELOG.md file rendered in preview mode with proper markdown formatting—version sections, issue links, and structured headings are all displaying correctly in dark theme. The PR description images demonstrate the README preview working beautifully with the Sourcebot branding, badges, and features section.

Thanks for the screenshot and the +1! The markdown preview feature is working well. Just to clarify—are you planning to address the theme-switching concern I flagged (the hardcoded bg-white and unconditional dark class), or would you like me to help with that implementation? Happy to provide guidance on wiring this up to next-themes if needed.


🧠 Learnings used
Learnt from: CR
Repo: sourcebot-dev/sourcebot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-05T22:59:12.488Z
Learning: Applies to **/*.{jsx,tsx} : Use Tailwind color classes directly (e.g., `border-border`, `bg-card`, `text-foreground`) instead of CSS variable syntax (e.g., `border-[var(--border)]`)

Comment on lines +27 to +29
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeHighlight]}
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Security: rehype-raw without rehype-sanitize enables XSS.

rehypeRaw passes raw HTML through to the rendered output. Without rehypeSanitize (already a project dependency), any markdown source containing <script>, <iframe>, onerror handlers, etc., will execute in the user's browser. This is especially dangerous since the markdown content comes from external repositories.

🔒 Proposed fix: add rehype-sanitize
 import rehypeHighlight from "rehype-highlight";
 import "github-markdown-css/github-markdown.css";
 import "highlight.js/styles/github-dark.css";
 import rehypeRaw from "rehype-raw";
+import rehypeSanitize, { defaultSchema } from "rehype-sanitize";

Then update the plugins array — rehypeSanitize must come after rehypeRaw:

-                        rehypePlugins={[rehypeRaw, rehypeHighlight]}
+                        rehypePlugins={[rehypeRaw, rehypeSanitize, rehypeHighlight]}

You may need to extend defaultSchema to allowlist elements like <video>, <source>, etc., that your custom components rely on.

🤖 Prompt for AI Agents
In
`@packages/web/src/app/`[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx
around lines 27 - 29, The ReactMarkdown rendering currently uses rehypeRaw
without sanitization (see ReactMarkdown component and the rehypePlugins array
containing rehypeRaw and rehypeHighlight); import and add rehypeSanitize to the
rehypePlugins list immediately after rehypeRaw so raw HTML is sanitized before
rendering, and if your UI requires additional tags/attributes (e.g., video,
source, iframe attrs), extend rehypeSanitize's schema (starting from
defaultSchema) to allowlist those elements/attributes before passing it to
rehypeSanitize.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brendan-kellam Thanks for acknowledging this! Given the critical nature of the XSS vulnerability, I'd recommend addressing this before merging the PR. The fix is straightforward—just adding rehype-sanitize to the pipeline—but it's essential for safely rendering markdown from external repositories.

Comment on lines +86 to +99
video: ({ src = "", ...props }) => {
return (
<video
src={src}
controls
preload="metadata"
className="max-w-full h-auto my-4"
{...props}
>
Your browser does not support the video
tag.
</video>
);
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Video src URLs are not resolved for relative paths.

The img and source handlers rewrite relative URLs using IMAGE_BASE_URL, but the video handler passes src through as-is. Relative video paths in markdown will be broken.

🤖 Prompt for AI Agents
In
`@packages/web/src/app/`[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx
around lines 86 - 99, The video renderer in pureMarkDownPreviewPanel.tsx (the
video: ({ src = "", ...props }) => { ... } handler) currently passes src through
unchanged, so relative video paths break; update this handler to rewrite
relative src the same way the img and source handlers do (use the same
IMAGE_BASE_URL or the existing resolve logic used for img/source) before
rendering the <video>, ensuring the src is resolved for both absolute and
relative URLs while preserving {...props} and controls.

@Naresh-official Naresh-official marked this pull request as draft February 7, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants