[object Object]

← back to Exo

Fix copy code button not working in dashboard (#1659)

3a4d635d0c07a2d851d551ccc4d724d990edad87 · 2026-03-05 20:41:17 +0800 · wysie

Fixes #1657

## Motivation

The copy button on code blocks in the dashboard does nothing when
clicked. This affects all code blocks in assistant responses.

## Root Cause

In `MarkdownContent.svelte`, click event listeners are bound to
`.copy-code-btn` elements via `setupCopyButtons()` inside a Svelte 5
`$effect`. However, the effect fires before the DOM has been updated
with the new HTML, so `querySelectorAll(".copy-code-btn")` finds zero
buttons.

Additionally, during streaming, the `content` prop updates on every
token, causing the entire `{@html processedHtml}` to be re-rendered.
This destroys all previously bound event listeners, even if they were
successfully attached.

## Changes

Replaced the per-button `addEventListener` approach with **event
delegation** — a single click listener on the container element that
catches clicks bubbling up from any `.copy-code-btn` or
`.copy-math-btn`. This:

- Eliminates the timing issue (the listener exists before the buttons
are rendered)
- Survives HTML re-renders during streaming (no need to re-bind)
- Removes the need for `setupCopyButtons()` and the `data-listenerBound`
tracking

## Testing

1. Load any model
2. Prompt it to generate a code block (e.g. "write a hello world in
Python")
3. Click the copy button on the code block
4. Paste — the code is copied correctly
5. Verified the button also works during streaming (before generation
completes)

Co-authored-by: Wysie <wysie@users.noreply.github.com>

Files touched

Diff

commit 3a4d635d0c07a2d851d551ccc4d724d990edad87
Author: wysie <sohyuanchin@gmail.com>
Date:   Thu Mar 5 20:41:17 2026 +0800

    Fix copy code button not working in dashboard (#1659)
    
    Fixes #1657
    
    ## Motivation
    
    The copy button on code blocks in the dashboard does nothing when
    clicked. This affects all code blocks in assistant responses.
    
    ## Root Cause
    
    In `MarkdownContent.svelte`, click event listeners are bound to
    `.copy-code-btn` elements via `setupCopyButtons()` inside a Svelte 5
    `$effect`. However, the effect fires before the DOM has been updated
    with the new HTML, so `querySelectorAll(".copy-code-btn")` finds zero
    buttons.
    
    Additionally, during streaming, the `content` prop updates on every
    token, causing the entire `{@html processedHtml}` to be re-rendered.
    This destroys all previously bound event listeners, even if they were
    successfully attached.
    
    ## Changes
    
    Replaced the per-button `addEventListener` approach with **event
    delegation** — a single click listener on the container element that
    catches clicks bubbling up from any `.copy-code-btn` or
    `.copy-math-btn`. This:
    
    - Eliminates the timing issue (the listener exists before the buttons
    are rendered)
    - Survives HTML re-renders during streaming (no need to re-bind)
    - Removes the need for `setupCopyButtons()` and the `data-listenerBound`
    tracking
    
    ## Testing
    
    1. Load any model
    2. Prompt it to generate a code block (e.g. "write a hello world in
    Python")
    3. Click the copy button on the code block
    4. Paste — the code is copied correctly
    5. Verified the button also works during streaming (before generation
    completes)
    
    Co-authored-by: Wysie <wysie@users.noreply.github.com>
---
 .../src/lib/components/MarkdownContent.svelte      | 24 ++++++++++++++++++++--
 1 file changed, 22 insertions(+), 2 deletions(-)

diff --git a/dashboard/src/lib/components/MarkdownContent.svelte b/dashboard/src/lib/components/MarkdownContent.svelte
index 1f83ac0c..e7aa3d50 100644
--- a/dashboard/src/lib/components/MarkdownContent.svelte
+++ b/dashboard/src/lib/components/MarkdownContent.svelte
@@ -507,9 +507,29 @@
   });
 
   $effect(() => {
-    if (containerRef && processedHtml) {
-      setupCopyButtons();
+    if (!containerRef || !browser) return;
+
+    function handleDelegatedClick(event: MouseEvent) {
+      const codeBtn = (event.target as HTMLElement).closest(
+        ".copy-code-btn",
+      ) as HTMLButtonElement | null;
+      if (codeBtn) {
+        handleCopyClick({ currentTarget: codeBtn } as unknown as Event);
+        return;
+      }
+      const mathBtn = (event.target as HTMLElement).closest(
+        ".copy-math-btn",
+      ) as HTMLButtonElement | null;
+      if (mathBtn) {
+        handleMathCopyClick({ currentTarget: mathBtn } as unknown as Event);
+        return;
+      }
     }
+
+    containerRef.addEventListener("click", handleDelegatedClick);
+    return () => {
+      containerRef?.removeEventListener("click", handleDelegatedClick);
+    };
   });
 </script>
 

← 84858050 fix(worker): emit error chunks when a runner dies mid-comman  ·  back to Exo  ·  Ciaran/re download bug (#1658) b9d40e8e →