collapsible responses rework
3clyp50 committed
Jan 29, 2026 at 09:35 UTC
d912caae61fa0b4fa026ee98fe7fef7b964a6d0b
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
@@ -518,45 +518,8 @@ function drawStandaloneMessage({
518
mainClass,
519
});
520
521
- // Collapsible: show ~10 lines with fade, expand button reveals full content
522
- messageDiv.classList.add("message-collapsible");
523
-
524
- const expandBtn = ensureChild(
525
- messageDiv,
526
- ".expand-btn",
527
- "button",
528
- "expand-btn",
529
- );
530
- expandBtn.textContent = messageDiv.classList.contains("expanded")
531
- ? "Show less"
532
- : "Show more";
533
- expandBtn.onclick = () => {
534
- messageDiv.classList.toggle("expanded");
535
- expandBtn.textContent = messageDiv.classList.contains("expanded")
536
- ? "Show less"
537
- : "Show more";
538
- };
539
-
540
- // Detect overflow after render - CSS handles visibility based on .has-overflow class
541
- requestAnimationFrame(() => {
542
- const body = messageDiv.querySelector(".message-body");
543
- messageDiv.classList.toggle(
544
- "has-overflow",
545
- body.scrollHeight > body.clientHeight,
546
- );
547
- });
548
-
549
- // Render action buttons: get/create container, clear, append
550
- const actionButtonsContainer = ensureChild(
551
- messageDiv,
552
- ".step-action-buttons",
553
- "div",
554
- "step-action-buttons",
555
- );
556
- actionButtonsContainer.textContent = "";
557
- (actionButtons || [])
558
- .filter(Boolean)
559
- .forEach((button) => actionButtonsContainer.appendChild(button));
521
+ // Collapsible with action buttons
522
+ setupCollapsible(messageDiv, ".step-action-buttons", false, actionButtons);
523
524
return container;
525
}
@@ -581,8 +544,10 @@ export function _drawMessage({
544
messageContainer.appendChild(messageDiv);
545
}
546
584
- // Update message classes
585
- messageDiv.className = `message ${mainClass} ${messageClasses.join(" ")}`;
547
+ // Update message classes (preserve collapsible state)
548
+ const preserve = ["message-collapsible", "expanded", "has-overflow"]
549
+ .filter((c) => messageDiv.classList.contains(c)).join(" ");
550
+ messageDiv.className = `message ${mainClass} ${messageClasses.join(" ")} ${preserve}`;
551
552
// Handle heading (important for error/rate_limit messages that show context)
553
if (heading) {
@@ -855,35 +820,7 @@ export function drawMessageResponse({
820
mainClass: "message-agent-response",
821
});
822
858
- // Collapsible: show ~10 lines with fade, expand button reveals full content
859
- messageDiv.classList.add("message-collapsible");
860
-
861
- const expandBtn = ensureChild(
862
- messageDiv,
863
- ".expand-btn",
864
- "button",
865
- "expand-btn",
866
- );
867
- expandBtn.textContent = messageDiv.classList.contains("expanded")
868
- ? "Show less"
869
- : "Show more";
870
- expandBtn.onclick = () => {
871
- messageDiv.classList.toggle("expanded");
872
- expandBtn.textContent = messageDiv.classList.contains("expanded")
873
- ? "Show less"
874
- : "Show more";
875
- };
876
-
877
- // Detect overflow after render - CSS handles visibility based on .has-overflow class
878
- requestAnimationFrame(() => {
879
- const body = messageDiv.querySelector(".message-body");
880
- messageDiv.classList.toggle(
881
- "has-overflow",
882
- body.scrollHeight > body.clientHeight,
883
- );
884
- });
885
-
886
- // Render action buttons: get/create container, clear, append
823
+ // Collapsible with action buttons
824
const responseText = String(content ?? "");
825
const responseActionButtons = responseText.trim()
826
? [
@@ -891,17 +828,7 @@ export function drawMessageResponse({
828
createActionButton("copy", "", () => copyToClipboard(responseText)),
829
].filter(Boolean)
830
: [];
894
- // Look for direct child only to avoid finding nested code block buttons
895
- const actionButtonsContainer = ensureChild(
896
- messageDiv,
897
- ":scope > .step-action-buttons",
898
- "div",
899
- "step-action-buttons",
900
- );
901
- actionButtonsContainer.textContent = "";
902
- responseActionButtons.forEach((button) =>
903
- actionButtonsContainer.appendChild(button),
904
- );
831
+ setupCollapsible(messageDiv, ":scope > .step-action-buttons", !isMassRender(), responseActionButtons);
832
833
if (group) updateProcessGroupHeader(group);
834
@@ -2199,6 +2126,38 @@ function ensureChild(parent, selector, tagName, ...classNames) {
2126
return el;
2127
}
2128
2129
+// Setup collapsible message with expand button and action buttons
2130
+function setupCollapsible(messageDiv, containerSelector, initialExpanded, actionButtons = []) {
2131
+ messageDiv.classList.add("message-collapsible");
2132
+ messageDiv.classList.toggle("expanded", initialExpanded);
2133
+
2134
+ const container = ensureChild(messageDiv, containerSelector, "div", "step-action-buttons");
2135
+ container.textContent = "";
2136
+
2137
+ const btn = ensureChild(container, ".expand-btn", "button", "expand-btn");
2138
+ const syncBtn = () => {
2139
+ const exp = messageDiv.classList.contains("expanded");
2140
+ btn.textContent = exp ? "Show less" : "Show more";
2141
+ btn.classList.toggle("show-less-btn", exp);
2142
+ btn.classList.toggle("show-more-btn", !exp);
2143
+ };
2144
+ syncBtn();
2145
+ btn.onclick = () => { messageDiv.classList.toggle("expanded"); syncBtn(); };
2146
+
2147
+ actionButtons.filter(Boolean).forEach((b) => container.appendChild(b));
2148
+
2149
+ // Detect overflow after render (skip if already detected to avoid scroll disruption)
2150
+ if (!messageDiv.classList.contains("has-overflow")) {
2151
+ requestAnimationFrame(() => {
2152
+ const body = messageDiv.querySelector(".message-body");
2153
+ const wasExp = messageDiv.classList.contains("expanded");
2154
+ messageDiv.classList.remove("expanded");
2155
+ messageDiv.classList.toggle("has-overflow", body?.scrollHeight > body?.clientHeight);
2156
+ messageDiv.classList.toggle("expanded", wasExp);
2157
+ });
2158
+ }
2159
+}
2160
+
2161
// returns true if this is the initial render of a chat eg. when reloading window, switching chat or catching up after a break
2162
// returns false when already in a rendered chat and adding messages regurarly
2163
function isMassRender() {