← 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
M dashboard/src/lib/components/MarkdownContent.svelte
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 →