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() {