add copy btn to code blocks and tables
3clyp50 committed
Jan 27, 2026 at 18:07 UTC
d189156a1658726752d41118275b9c44c33a888b
3 files changed
+83
-13
webui/components/messages/action-buttons/simple-action-buttons.css
+17
-5
@@ -14,7 +14,8 @@
14
pointer-events: none;
15
}
16
17
-.step-action-buttons .action-button {
17
+.step-action-buttons .action-button,
18
+.action-button {
19
display: flex;
20
align-items: center;
21
justify-content: center;
@@ -24,7 +25,8 @@
25
cursor: pointer;
26
}
27
27
-.step-action-buttons .action-button .material-symbols-outlined {
28
+.step-action-buttons .action-button .material-symbols-outlined,
29
+.action-button .material-symbols-outlined {
30
font-size: 0.85rem;
31
line-height: 1;
32
color: var(--color-message-text);
@@ -32,18 +34,21 @@
34
transition: color 0.15s ease;
35
}
36
35
-.step-action-buttons .action-button:hover .material-symbols-outlined {
37
+.step-action-buttons .action-button:hover .material-symbols-outlined,
38
+.action-button:hover .material-symbols-outlined {
39
color: var(--color-text);
40
}
41
42
/* Success state */
40
-.step-action-buttons .action-button.success .material-symbols-outlined {
43
+.step-action-buttons .action-button.success .material-symbols-outlined,
44
+.action-button.success .material-symbols-outlined {
45
color: #4CAF50;
46
font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
47
}
48
49
/* Error state */
46
-.step-action-buttons .action-button.error .material-symbols-outlined {
50
+.step-action-buttons .action-button.error .material-symbols-outlined,
51
+.action-button.error .material-symbols-outlined {
52
color: var(--color-accent);
53
font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
54
}
@@ -65,6 +70,13 @@
70
pointer-events: auto;
71
}
72
73
+/* Code blocks and tables: show copy button on hover */
74
+.device-pointer .code-block-wrapper:hover .step-action-buttons,
75
+.device-pointer .message-markdown-table-wrap:hover .step-action-buttons {
76
+ opacity: 1;
77
+ pointer-events: auto;
78
+}
79
+
80
/* ===========================================
81
Touch devices - Always visible
82
=========================================== */
webui/css/messages.css
+17
@@ -732,6 +732,23 @@
732
border-radius: 0.3em;
733
}
734
735
+/* Code block and table copy buttons */
736
+.code-block-wrapper,
737
+.message-markdown-table-wrap {
738
+ position: relative;
739
+}
740
+
741
+.overlay-actions {
742
+ position: absolute;
743
+ top: 0;
744
+ right: 0;
745
+ background: var(--color-panel);
746
+ border-radius: var(--border-radius-sm);
747
+ z-index: 1;
748
+ padding: 2px !important;
749
+ gap: 0 !important;
750
+}
751
+
752
.msg-content hr {
753
border: 0;
754
border-top: 1px solid var(--color-border);
webui/js/messages.js
+49
-8
@@ -1883,16 +1883,57 @@ function convertPathsToLinks(str) {
1883
.join("");
1884
}
1885
1886
+// markdown render helpers //
1887
+
1888
+// wraps an element with a container div
1889
+const wrapElement = (el, className) => {
1890
+ const wrapper = document.createElement("div");
1891
+ wrapper.className = className;
1892
+ el.parentNode.insertBefore(wrapper, el);
1893
+ wrapper.appendChild(el);
1894
+ return wrapper;
1895
+};
1896
+
1897
+// data extractors
1898
+const extractTableTSV = (table) =>
1899
+ [...table.rows]
1900
+ .map((row) =>
1901
+ [...row.cells]
1902
+ .map((cell) => cell.textContent.replace(/\t/g, " ").replace(/\n/g, " "))
1903
+ .join("\t"),
1904
+ )
1905
+ .join("\n");
1906
+
1907
function adjustMarkdownRender(element) {
1908
// find all tables in the element
1888
- const elements = element.querySelectorAll("table");
1889
-
1890
- // wrap each with a div with class message-markdown-table-wrap
1891
- elements.forEach((el) => {
1892
- const wrapper = document.createElement("div");
1893
- wrapper.className = "message-markdown-table-wrap";
1894
- el.parentNode.insertBefore(wrapper, el);
1895
- wrapper.appendChild(el);
1909
+ const tables = element.querySelectorAll("table");
1910
+ tables.forEach((el) => {
1911
+ if (el.parentNode.classList.contains("message-markdown-table-wrap"))
1912
+ return;
1913
+ const wrapper = wrapElement(el, "message-markdown-table-wrap");
1914
+
1915
+ // create copy button directly
1916
+ const copyBtn = createActionButton("copy", "", () =>
1917
+ copyToClipboard(extractTableTSV(el))
1918
+ );
1919
+ copyBtn.classList.add("step-action-buttons", "overlay-actions");
1920
+ wrapper.appendChild(copyBtn);
1921
+ });
1922
+
1923
+ // find all code blocks
1924
+ // we select the code element to ensure valid targets, then wrap the parent pre
1925
+ const codeElements = element.querySelectorAll("pre > code");
1926
+ codeElements.forEach((code) => {
1927
+ const pre = code.parentNode;
1928
+ if (pre.parentNode.classList.contains("code-block-wrapper")) return;
1929
+ const wrapper = wrapElement(pre, "code-block-wrapper");
1930
+
1931
+ // create copy button directly
1932
+ const copyBtn = createActionButton("copy", "", () =>
1933
+ copyToClipboard(code.textContent)
1934
+ );
1935
+ copyBtn.classList.add("step-action-buttons", "overlay-actions");
1936
+ wrapper.appendChild(copyBtn);
1937
});
1938
1939
// find all images