collapsible responses rework
3clyp50 committed
Jan 29, 2026 at 09:35 UTC
3805cc443943bea3b80616103a082b4b94af8b8f
3 files changed
+108
-102
webui/components/messages/action-buttons/simple-action-buttons.css
+68
@@ -14,6 +14,24 @@
14
pointer-events: none;
15
}
16
17
+.step-action-buttons .expand-btn {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ background: transparent;
21
+ border: none;
22
+ color: var(--color-text-muted);
23
+ font-family: "Rubik", Arial, Helvetica, sans-serif;
24
+ font-size: var(--font-size-xs);
25
+ cursor: pointer;
26
+ padding: var(--spacing-xs) 0;
27
+ opacity: 0.7;
28
+ transition: opacity 0.15s ease;
29
+}
30
+
31
+.step-action-buttons .expand-btn:hover {
32
+ opacity: 1;
33
+}
34
+
35
.step-action-buttons .action-button,
36
.action-button {
37
display: flex;
@@ -53,6 +71,55 @@
71
font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
72
}
73
74
+.show-more-btn,
75
+.show-less-btn {
76
+ display: none;
77
+}
78
+
79
+.message.message-collapsible.has-overflow:not(.expanded) > .step-action-buttons {
80
+ opacity: 1;
81
+ pointer-events: auto;
82
+}
83
+
84
+.message.message-collapsible.has-overflow:not(.expanded)
85
+ > .step-action-buttons
86
+ .show-more-btn {
87
+ display: inline-flex;
88
+ pointer-events: auto;
89
+}
90
+
91
+.device-pointer
92
+ .message.message-collapsible.has-overflow:not(.expanded)
93
+ > .step-action-buttons
94
+ .action-button {
95
+ opacity: 0;
96
+ pointer-events: none;
97
+}
98
+
99
+.device-pointer
100
+ .message.message-collapsible.has-overflow:not(.expanded):hover
101
+ > .step-action-buttons
102
+ .action-button {
103
+ opacity: 1;
104
+ pointer-events: auto;
105
+}
106
+
107
+.device-pointer
108
+ .message.message-collapsible.has-overflow.expanded:hover
109
+ > .step-action-buttons
110
+ .show-less-btn {
111
+ display: inline-flex;
112
+ pointer-events: auto;
113
+}
114
+
115
+.device-touch
116
+ .message.message-collapsible.has-overflow.expanded
117
+ > .step-action-buttons
118
+ .show-less-btn {
119
+ display: inline-flex;
120
+ pointer-events: auto;
121
+}
122
+
123
/* User messages: right-aligned */
124
.message-user .step-action-buttons {
125
justify-content: flex-end;
@@ -70,6 +137,7 @@
137
138
.device-pointer .message-user:hover > .step-action-buttons,
139
.device-pointer .message-agent-response:hover > .step-action-buttons,
140
+.device-pointer .message.message-collapsible.expanded:hover > .step-action-buttons,
141
.device-pointer .process-step:hover > .process-step-detail .step-action-buttons {
142
opacity: 1;
143
pointer-events: auto;
webui/css/messages.css
-21
@@ -846,24 +846,3 @@
846
opacity: 0;
847
}
848
849
-/* Expand button - hidden by default, shown when overflow */
850
-.message.message-collapsible .expand-btn {
851
- display: none;
852
- background: transparent;
853
- border: none;
854
- color: var(--color-text-muted);
855
- font-family: "Rubik", Arial, Helvetica, sans-serif;
856
- font-size: var(--font-size-xs);
857
- cursor: pointer;
858
- padding: var(--spacing-xs) 0;
859
- opacity: 0.7;
860
- transition: opacity 0.15s ease;
861
-}
862
-
863
-.message.message-collapsible .expand-btn:hover {
864
- opacity: 1;
865
-}
866
-
867
-.message.message-collapsible.has-overflow .expand-btn {
868
- display: block;
869
-}
webui/js/messages.js
+40
-81
@@ -439,45 +439,8 @@ function drawStandaloneMessage({
439
mainClass,
440
});
441
442
- // Collapsible: show ~10 lines with fade, expand button reveals full content
443
- messageDiv.classList.add("message-collapsible");
444
-
445
- const expandBtn = ensureChild(
446
- messageDiv,
447
- ".expand-btn",
448
- "button",
449
- "expand-btn",
450
- );
451
- expandBtn.textContent = messageDiv.classList.contains("expanded")
452
- ? "Show less"
453
- : "Show more";
454
- expandBtn.onclick = () => {
455
- messageDiv.classList.toggle("expanded");
456
- expandBtn.textContent = messageDiv.classList.contains("expanded")
457
- ? "Show less"
458
- : "Show more";
459
- };
460
-
461
- // Detect overflow after render - CSS handles visibility based on .has-overflow class
462
- requestAnimationFrame(() => {
463
- const body = messageDiv.querySelector(".message-body");
464
- messageDiv.classList.toggle(
465
- "has-overflow",
466
- body.scrollHeight > body.clientHeight,
467
- );
468
- });
469
-
470
- // Render action buttons: get/create container, clear, append
471
- const actionButtonsContainer = ensureChild(
472
- messageDiv,
473
- ".step-action-buttons",
474
- "div",
475
- "step-action-buttons",
476
- );
477
- actionButtonsContainer.textContent = "";
478
- (actionButtons || [])
479
- .filter(Boolean)
480
- .forEach((button) => actionButtonsContainer.appendChild(button));
442
+ // Collapsible with action buttons
443
+ setupCollapsible(messageDiv, ".step-action-buttons", false, actionButtons);
444
445
return container;
446
}
@@ -502,8 +465,10 @@ export function _drawMessage({
465
messageContainer.appendChild(messageDiv);
466
}
467
505
- // Update message classes
506
- messageDiv.className = `message ${mainClass} ${messageClasses.join(" ")}`;
468
+ // Update message classes (preserve collapsible state)
469
+ const preserve = ["message-collapsible", "expanded", "has-overflow"]
470
+ .filter((c) => messageDiv.classList.contains(c)).join(" ");
471
+ messageDiv.className = `message ${mainClass} ${messageClasses.join(" ")} ${preserve}`;
472
473
// Handle heading (important for error/rate_limit messages that show context)
474
if (heading) {
@@ -774,35 +739,7 @@ export function drawMessageResponse({
739
mainClass: "message-agent-response",
740
});
741
777
- // Collapsible: show ~10 lines with fade, expand button reveals full content
778
- messageDiv.classList.add("message-collapsible");
779
-
780
- const expandBtn = ensureChild(
781
- messageDiv,
782
- ".expand-btn",
783
- "button",
784
- "expand-btn",
785
- );
786
- expandBtn.textContent = messageDiv.classList.contains("expanded")
787
- ? "Show less"
788
- : "Show more";
789
- expandBtn.onclick = () => {
790
- messageDiv.classList.toggle("expanded");
791
- expandBtn.textContent = messageDiv.classList.contains("expanded")
792
- ? "Show less"
793
- : "Show more";
794
- };
795
-
796
- // Detect overflow after render - CSS handles visibility based on .has-overflow class
797
- requestAnimationFrame(() => {
798
- const body = messageDiv.querySelector(".message-body");
799
- messageDiv.classList.toggle(
800
- "has-overflow",
801
- body.scrollHeight > body.clientHeight,
802
- );
803
- });
804
-
805
- // Render action buttons: get/create container, clear, append
742
+ // Collapsible with action buttons
743
const responseText = String(content ?? "");
744
const responseActionButtons = responseText.trim()
745
? [
@@ -810,17 +747,7 @@ export function drawMessageResponse({
747
createActionButton("copy", "", () => copyToClipboard(responseText)),
748
].filter(Boolean)
749
: [];
813
- // Look for direct child only to avoid finding nested code block buttons
814
- const actionButtonsContainer = ensureChild(
815
- messageDiv,
816
- ":scope > .step-action-buttons",
817
- "div",
818
- "step-action-buttons",
819
- );
820
- actionButtonsContainer.textContent = "";
821
- responseActionButtons.forEach((button) =>
822
- actionButtonsContainer.appendChild(button),
823
- );
750
+ setupCollapsible(messageDiv, ":scope > .step-action-buttons", !isMassRender(), responseActionButtons);
751
752
if (group) updateProcessGroupHeader(group);
753
@@ -2116,6 +2043,38 @@ function ensureChild(parent, selector, tagName, ...classNames) {
2043
return el;
2044
}
2045
2046
+// Setup collapsible message with expand button and action buttons
2047
+function setupCollapsible(messageDiv, containerSelector, initialExpanded, actionButtons = []) {
2048
+ messageDiv.classList.add("message-collapsible");
2049
+ messageDiv.classList.toggle("expanded", initialExpanded);
2050
+
2051
+ const container = ensureChild(messageDiv, containerSelector, "div", "step-action-buttons");
2052
+ container.textContent = "";
2053
+
2054
+ const btn = ensureChild(container, ".expand-btn", "button", "expand-btn");
2055
+ const syncBtn = () => {
2056
+ const exp = messageDiv.classList.contains("expanded");
2057
+ btn.textContent = exp ? "Show less" : "Show more";
2058
+ btn.classList.toggle("show-less-btn", exp);
2059
+ btn.classList.toggle("show-more-btn", !exp);
2060
+ };
2061
+ syncBtn();
2062
+ btn.onclick = () => { messageDiv.classList.toggle("expanded"); syncBtn(); };
2063
+
2064
+ actionButtons.filter(Boolean).forEach((b) => container.appendChild(b));
2065
+
2066
+ // Detect overflow after render (skip if already detected to avoid scroll disruption)
2067
+ if (!messageDiv.classList.contains("has-overflow")) {
2068
+ requestAnimationFrame(() => {
2069
+ const body = messageDiv.querySelector(".message-body");
2070
+ const wasExp = messageDiv.classList.contains("expanded");
2071
+ messageDiv.classList.remove("expanded");
2072
+ messageDiv.classList.toggle("has-overflow", body?.scrollHeight > body?.clientHeight);
2073
+ messageDiv.classList.toggle("expanded", wasExp);
2074
+ });
2075
+ }
2076
+}
2077
+
2078
// returns true if this is the initial render of a chat eg. when reloading window, switching chat or catching up after a break
2079
// returns false when already in a rendered chat and adding messages regurarly
2080
function isMassRender() {