diff --git a/package.json b/package.json index 0efc775e..f17cfdab 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "radix-ui": "^1.4.3", "react-markdown": "^10.1.0", "rehype-highlight": "^7.0.2", + "remark-breaks": "^4.0.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", "shadcn": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01cf0521..5efda2ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,9 @@ importers: rehype-highlight: specifier: ^7.0.2 version: 7.0.2 + remark-breaks: + specifier: ^4.0.0 + version: 4.0.0 remark-frontmatter: specifier: ^5.0.0 version: 5.0.0 @@ -4370,6 +4373,9 @@ packages: mdast-util-mdxjs-esm@2.0.1: resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + mdast-util-newline-to-break@2.0.0: + resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==} + mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} @@ -5058,6 +5064,9 @@ packages: rehype-highlight@7.0.2: resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==} + remark-breaks@4.0.0: + resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} + remark-frontmatter@5.0.0: resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} @@ -10141,6 +10150,11 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-newline-to-break@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-find-and-replace: 3.0.2 + mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 @@ -11124,6 +11138,12 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 + remark-breaks@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-newline-to-break: 2.0.0 + unified: 11.0.5 + remark-frontmatter@5.0.0: dependencies: '@types/mdast': 4.0.4 diff --git a/src/renderer/src/components/sidebar/CommentMarkdown.tsx b/src/renderer/src/components/sidebar/CommentMarkdown.tsx new file mode 100644 index 00000000..aa051567 --- /dev/null +++ b/src/renderer/src/components/sidebar/CommentMarkdown.tsx @@ -0,0 +1,132 @@ +import React from 'react' +import Markdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import remarkBreaks from 'remark-breaks' +import type { Components } from 'react-markdown' +import { cn } from '@/lib/utils' + +// Why: sidebar comments are rendered at 11px in a narrow card, so we strip +// block-level wrappers that add unwanted margins and only keep inline +// formatting (bold, italic, code, links) plus compact lists and line breaks. +// Using react-markdown (already a project dependency) lets AI agents write +// markdown via `orca worktree set --comment` and have it render nicely. + +const components: Components = { + // Strip
wrappers to avoid double margins in the tight card layout.
+ p: ({ children }) => {children},
+ // Open links externally — sidebar is not a navigation context.
+ a: ({ href, children }) => (
+ e.stopPropagation()}
+ >
+ {children}
+
+ ),
+ // Why: react-markdown calls the `code` component for both inline `code`
+ // and the
- Press Enter or {isMac ? 'Cmd' : 'Ctrl'}+Enter to save, Shift+Enter for a new line.
+ Supports **markdown** — bold, lists, `code`, links. Press Enter or{' '}
+ {isMac ? 'Cmd' : 'Ctrl'}+Enter to save, Shift+Enter for a new line.
inside fenced blocks (
). We
+ // always apply inline-code styling here; the wrapper div uses a CSS
+ // descendant selector ([&_pre_code]) at higher specificity to strip
+ // the pill background/padding when code is inside a …. This is
+ // more reliable than checking `className` — which is only set when
+ // the fenced block specifies a language (```js), not for bare ```.
+ code: ({ children }) => (
+ {children}
+ ),
+ // Compact pre blocks — no syntax highlighting needed for short comments
+ pre: ({ children }) => (
+
+ {children}
+
+ ),
+ // Compact lists
+ ul: ({ children }) => {children}
,
+ ol: ({ children }) => {children}
,
+ // Why: GFM task list checkboxes are non-functional in a read-only comment
+ // card (clicking them would just open the edit modal via the parent's
+ // onClick). Rendering them disabled avoids a misleading interactive
+ // affordance.
+ li: ({ children }) => (
+
,
+ // Compact blockquotes
+ blockquote: ({ children }) => (
+
+ {children}
+
+ ),
+ // Why: images in a ~200px sidebar card would blow out the layout or look
+ // broken at any reasonable size. Render as a text link instead so the URL is
+ // still accessible without disrupting the card.
+ img: ({ alt, src }) => (
+ e.stopPropagation()}
+ >
+ {alt || 'image'}
+
+ ),
+ // Why: GFM tables in a ~200px sidebar would overflow badly. Wrapping in an
+ // overflow container keeps the card layout stable while still letting the
+ // user scroll to see the full table.
+ table: ({ children }) => (
+
+ {children}
+
+
, keeping backward compat
+// with existing plain-text comments that rely on newline formatting.
+const remarkPlugins = [remarkGfm, remarkBreaks]
+
+type CommentMarkdownProps = {
+ content: string
+ className?: string
+ onClick?: (e: React.MouseEvent) => void
+}
+
+const CommentMarkdown = React.memo(function CommentMarkdown({
+ content,
+ className,
+ onClick
+}: CommentMarkdownProps) {
+ return (
+ block.
+ // The descendant selector (pre code) has higher specificity than the
+ // direct utility classes on
, so these overrides win reliably.
+ '[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:rounded-none',
+ className
+ )}
+ onClick={onClick}
+ >
+