Skip to content

[Feature Request] Add a copy button to code blocks #2478

Description

@dungphan0038

Problem

When viewing long code blocks in the chat, users currently have to manually select the text to copy it. This is inconvenient, especially for long commands, configuration files, or multi-line code snippets.

Proposed Solution

Add a small copy icon button to each code block, similar to GitHub's implementation. The button should:

  • Appear on hover at the top-right corner (fade in)
  • Copy the full code block content when clicked
  • Show a brief checkmark confirmation after copying

Workaround (DevTools Snippet)

Until this is implemented natively, you can use the following DevTools Snippet. Open DevTools (Ctrl + Shift + I) → SourcesSnippets, create a new snippet, paste the code below, save, and run it.

Note: Works best with OpenWork's dark theme.

(function() {
    // Icon SVG copy (GitHub-style)
    const iconCopy = `<svg class="copy-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg>`;

    const iconCheck = `<svg class="copy-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/></svg>`;

    // Style
    const style = document.createElement('style');
    style.textContent = `
        .code-block-wrapper {
            position: relative;
        }

        .copy-btn {
            position: absolute;
            top: 8px;
            right: 8px;
            z-index: 10;
            display: flex;
            align-items: center;
            justify-content: center;
            width: 32px;
            height: 32px;
            padding: 4px;
            color: rgba(255,255,255,0.4);
            background: transparent;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            opacity: 0;
            transition: opacity 0.2s ease, color 0.2s ease, background 0.2s ease;
        }

        .code-block-wrapper:hover .copy-btn {
            opacity: 1;
        }

        .copy-btn:hover {
            color: rgba(255,255,255,0.8);
            background: rgba(255,255,255,0.08);
        }

        .copy-btn.copied {
            color: #00c853;
        }
    `;
    document.head.appendChild(style);

    function addCopyButtons() {
        document.querySelectorAll('pre:not([data-has-copy])').forEach(pre => {
            pre.dataset.hasCopy = 'true';

            const wrapper = document.createElement('div');
            wrapper.className = 'code-block-wrapper';
            pre.parentNode.insertBefore(wrapper, pre);
            wrapper.appendChild(pre);

            const btn = document.createElement('button');
            btn.className = 'copy-btn';
            btn.innerHTML = iconCopy;

            btn.addEventListener('click', async () => {
                try {
                    await navigator.clipboard.writeText(pre.textContent);
                    btn.innerHTML = iconCheck;
                    btn.classList.add('copied');
                    setTimeout(() => {
                        btn.innerHTML = iconCopy;
                        btn.classList.remove('copied');
                    }, 2000);
                } catch {
                    btn.innerHTML = iconCopy;
                }
            });

            wrapper.appendChild(btn);
        });
    }

    addCopyButtons();

    const observer = new MutationObserver(addCopyButtons);
    observer.observe(document.body, { childList: true, subtree: true });

    console.log('✅ Copy code block button added!');
})();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions