From 299cb8502acebcc7cf2c9c07f73b60511add84ff Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 24 Sep 2026 11:35:52 -0700 Subject: [PATCH 1/2] refactor(emcn): share copy code header appearance --- .../message/components/markdown-renderer.tsx | 5 +- .../components/chat-content/chat-content.tsx | 5 +- .../components/code/copy-code-button.test.tsx | 56 +++++++++++++++++++ .../src/components/code/copy-code-button.tsx | 23 +++++++- packages/emcn/src/components/index.ts | 2 +- 5 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 packages/emcn/src/components/code/copy-code-button.test.tsx diff --git a/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx b/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx index dd6199e063e..8536d1ffc70 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx @@ -88,10 +88,7 @@ const COMPONENTS = { {codeProps.className?.replace('language-', '') || 'code'} - +
           {codeContent}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx
index dc3537b8e1e..641fa983fff 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx
@@ -315,10 +315,7 @@ const MARKDOWN_COMPONENTS = {
       
{language || 'code'} - +
diff --git a/packages/emcn/src/components/code/copy-code-button.test.tsx b/packages/emcn/src/components/code/copy-code-button.test.tsx
new file mode 100644
index 00000000000..2c751b9ba65
--- /dev/null
+++ b/packages/emcn/src/components/code/copy-code-button.test.tsx
@@ -0,0 +1,56 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { act } from 'react'
+import { createRoot } from 'react-dom/client'
+import { renderToStaticMarkup } from 'react-dom/server'
+import { describe, expect, it, vi } from 'vitest'
+import { CopyCodeButton } from './copy-code-button'
+
+describe('CopyCodeButton', () => {
+  it.each(['default', 'code-header'] as const)(
+    'names the icon-only button in the %s appearance',
+    (appearance) => {
+      const markup = renderToStaticMarkup()
+
+      expect(markup).toContain('aria-label="Copy code"')
+      expect(markup).toContain('type="button"')
+    }
+  )
+
+  it('copies code, swaps to the check icon, and keeps its accessible name', async () => {
+    ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+    const writeText = vi.fn().mockResolvedValue(undefined)
+    const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard')
+    Object.defineProperty(navigator, 'clipboard', {
+      configurable: true,
+      value: { writeText },
+    })
+    const host = document.createElement('div')
+    document.body.appendChild(host)
+    const root = createRoot(host)
+
+    try {
+      await act(async () => {
+        root.render()
+      })
+      const button = host.querySelector('button')
+      expect(button?.getAttribute('aria-label')).toBe('Copy code')
+      expect(button?.querySelector('svg rect')).not.toBeNull()
+
+      await act(async () => {
+        button?.click()
+      })
+
+      expect(writeText).toHaveBeenCalledExactlyOnceWith('example')
+      expect(button?.querySelector('svg rect')).toBeNull()
+      expect(button?.querySelector('svg path')).not.toBeNull()
+      expect(button?.getAttribute('aria-label')).toBe('Copy code')
+    } finally {
+      await act(async () => root.unmount())
+      host.remove()
+      if (originalClipboard) Object.defineProperty(navigator, 'clipboard', originalClipboard)
+      else Reflect.deleteProperty(navigator, 'clipboard')
+    }
+  })
+})
diff --git a/packages/emcn/src/components/code/copy-code-button.tsx b/packages/emcn/src/components/code/copy-code-button.tsx
index 2c1c30046b3..0be0fba0d85 100644
--- a/packages/emcn/src/components/code/copy-code-button.tsx
+++ b/packages/emcn/src/components/code/copy-code-button.tsx
@@ -1,25 +1,44 @@
 'use client'
 
+import { cva, type VariantProps } from 'class-variance-authority'
 import { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard'
 import { Button, Check, Duplicate } from '../../index'
 import { cn } from '../../lib/cn'
 
+const copyCodeButtonVariants = cva('flex items-center gap-1 rounded px-1.5 py-0.5 text-xs', {
+  variants: {
+    appearance: {
+      default: '',
+      'code-header':
+        'text-[var(--text-tertiary)] hover-hover:bg-[var(--surface-5)] hover-hover:text-[var(--text-secondary)]',
+    },
+  },
+  defaultVariants: {
+    appearance: 'default',
+  },
+})
+
 interface CopyCodeButtonProps {
   code: string
   className?: string
+  /** Use the muted copy action treatment in code block headers. */
+  appearance?: VariantProps['appearance']
 }
 
-export function CopyCodeButton({ code, className }: CopyCodeButtonProps) {
+export function CopyCodeButton({ code, className, appearance }: CopyCodeButtonProps) {
   const { copied, copy } = useCopyToClipboard()
 
   return (
     
   )
 }
+
+export { copyCodeButtonVariants }
diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts
index 8a6c0391020..f0994a75e23 100644
--- a/packages/emcn/src/components/index.ts
+++ b/packages/emcn/src/components/index.ts
@@ -117,7 +117,7 @@ export {
   calculateGutterWidth,
   getCodeEditorProps,
 } from './code/code'
-export { CopyCodeButton } from './code/copy-code-button'
+export { CopyCodeButton, copyCodeButtonVariants } from './code/copy-code-button'
 export { highlight, languages } from './code/prism'
 export { CollapsibleCard, type CollapsibleCardProps } from './collapsible-card/collapsible-card'
 export {

From 1312c6d158a1e00d668ab5476fc21f66100003d4 Mon Sep 17 00:00:00 2001
From: Bill Leoutsakos 
Date: Thu, 24 Sep 2026 12:16:37 -0700
Subject: [PATCH 2/2] test(emcn): assert copy code appearances

---
 .../components/code/copy-code-button.test.tsx | 24 +++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/packages/emcn/src/components/code/copy-code-button.test.tsx b/packages/emcn/src/components/code/copy-code-button.test.tsx
index 2c751b9ba65..ac5b4d3fe88 100644
--- a/packages/emcn/src/components/code/copy-code-button.test.tsx
+++ b/packages/emcn/src/components/code/copy-code-button.test.tsx
@@ -12,9 +12,33 @@ describe('CopyCodeButton', () => {
     'names the icon-only button in the %s appearance',
     (appearance) => {
       const markup = renderToStaticMarkup()
+      const host = document.createElement('div')
+      host.innerHTML = markup
+      const button = host.querySelector('button')
 
       expect(markup).toContain('aria-label="Copy code"')
       expect(markup).toContain('type="button"')
+      expect(button).not.toBeNull()
+      for (const className of [
+        'flex',
+        'items-center',
+        'gap-1',
+        'rounded',
+        'px-1.5',
+        'py-0.5',
+        'text-xs',
+      ]) {
+        expect(button?.classList.contains(className)).toBe(true)
+      }
+
+      const codeHeaderClasses = [
+        'text-[var(--text-tertiary)]',
+        'hover-hover:bg-[var(--surface-5)]',
+        'hover-hover:text-[var(--text-secondary)]',
+      ]
+      for (const className of codeHeaderClasses) {
+        expect(button?.classList.contains(className)).toBe(appearance === 'code-header')
+      }
     }
   )