diff --git a/src/browser/components/Messages/MarkdownComponents.tsx b/src/browser/components/Messages/MarkdownComponents.tsx index f08aa812a9..19d3c2852e 100644 --- a/src/browser/components/Messages/MarkdownComponents.tsx +++ b/src/browser/components/Messages/MarkdownComponents.tsx @@ -81,9 +81,10 @@ const CodeBlock: React.FC = ({ code, language }) => { }, [code, language, themeMode]); const lines = highlightedLines ?? plainLines; + const isSingleLine = lines.length === 1; return ( -
+
{lines.map((content, idx) => ( diff --git a/src/browser/stories/App.markdown.stories.tsx b/src/browser/stories/App.markdown.stories.tsx index 6b39ad2d71..d803f32fe0 100644 --- a/src/browser/stories/App.markdown.stories.tsx +++ b/src/browser/stories/App.markdown.stories.tsx @@ -75,6 +75,18 @@ ORDER BY time \`\`\` `; +const SINGLE_LINE_CODE = `Here's a one-liner: + +\`\`\`bash +npm install mux +\`\`\` + +And another: + +\`\`\`typescript +const x = 42; +\`\`\``; + const CODE_CONTENT = `Here's the implementation: \`\`\`typescript @@ -142,6 +154,59 @@ export const Tables: AppStory = { ), }; +/** Single-line code blocks - copy button should be compact */ +export const SingleLineCodeBlocks: AppStory = { + render: () => ( + + setupSimpleChatStory({ + workspaceId: "ws-single-line", + messages: [ + createUserMessage("msg-1", "Show me single-line code", { + historySequence: 1, + timestamp: STABLE_TIMESTAMP - 100000, + }), + createAssistantMessage("msg-2", SINGLE_LINE_CODE, { + historySequence: 2, + timestamp: STABLE_TIMESTAMP - 90000, + }), + ], + }) + } + /> + ), + play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + await waitForChatMessagesLoaded(canvasElement); + + // Wait for code blocks to render with highlighting + const codeWrappers = await waitFor( + () => { + const candidates = Array.from(canvasElement.querySelectorAll(".code-block-wrapper")); + if (candidates.length < 2) { + throw new Error("Not all code blocks rendered yet"); + } + return candidates as HTMLElement[]; + }, + { timeout: 5000 } + ); + + // Verify the first code block wrapper has only one line + const lineNumbers = codeWrappers[0].querySelectorAll(".line-number"); + await expect(lineNumbers.length).toBe(1); + + // Verify the single-line class is applied for compact styling + await expect(codeWrappers[0].classList.contains("code-block-single-line")).toBe(true); + + // Force copy buttons visible for screenshot (normally shown on hover) + for (const wrapper of codeWrappers) { + const copyButton = wrapper.querySelector(".code-copy-button"); + if (copyButton) { + copyButton.style.opacity = "1"; + } + } + }, +}; + /** SQL with double underscores in code block - tests for bug where __ leaks to end */ export const SqlWithDoubleUnderscore: AppStory = { render: () => ( diff --git a/src/browser/styles/globals.css b/src/browser/styles/globals.css index 59345aac83..d3fd806760 100644 --- a/src/browser/styles/globals.css +++ b/src/browser/styles/globals.css @@ -1604,13 +1604,13 @@ span.search-highlight { padding: 0; } -/* Reusable copy button styles */ +/* Reusable copy button styles - reuses line-number colors for theme consistency */ .copy-button { padding: 6px 8px; - background: rgba(0, 0, 0, 0.6); - border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--color-line-number-bg); + border: 1px solid var(--color-line-number-border); border-radius: 4px; - color: rgba(255, 255, 255, 0.6); + color: var(--color-line-number-text); cursor: pointer; transition: color 0.2s, @@ -1623,9 +1623,9 @@ span.search-highlight { } .copy-button:hover { - background: rgba(0, 0, 0, 0.8); - color: rgba(255, 255, 255, 0.9); - border-color: rgba(255, 255, 255, 0.2); + background: var(--color-code-bg); + color: var(--color-foreground); + border-color: var(--color-foreground); } .copy-icon { @@ -1635,7 +1635,7 @@ span.search-highlight { .copy-feedback { font-size: 11px; - color: rgba(255, 255, 255, 0.9); + color: var(--color-foreground); } /* Code block specific positioning */ @@ -1651,6 +1651,19 @@ span.search-highlight { opacity: 1; } +/* Single-line code block: inline copy button at top-right */ +.code-block-single-line .code-copy-button { + top: 50%; + bottom: auto; + transform: translateY(-50%); + padding: 2px 4px; +} + +.code-block-single-line .code-copy-button .copy-icon { + width: 12px; + height: 12px; +} + /* Markdown code blocks (fallback for non-highlighted blocks) */ pre code { display: block;