user message attachments progress
frdel committed
Jan 21, 2026 at 16:47 UTC
060f631f3ea64ec6c076a788b0248b8bb80f4476
8 files changed
+74
-107
python/api/api_message.py
+1
-1
@@ -136,7 +136,7 @@ class ApiMessage(ApiHandler):
136
# Add user message to chat history so it's visible in the UI
137
context.log.log(
138
type="user",
139
- heading="User message",
139
+ heading="",
140
content=message,
141
kvps={"attachments": attachment_filenames},
142
)
python/api/message.py
+1
-1
@@ -84,7 +84,7 @@ class Message(ApiHandler):
84
# Log the message with message_id and attachments
85
context.log.log(
86
type="user",
87
- heading="User message",
87
+ heading="",
88
content=message,
89
kvps={"attachments": attachment_filenames},
90
id=message_id,
python/helpers/task_scheduler.py
+1
-1
@@ -874,7 +874,7 @@ class TaskScheduler:
874
# Log the message with message_id and attachments
875
context.log.log(
876
type="user",
877
- heading="User message",
877
+ heading="",
878
content=task_prompt,
879
kvps={"attachments": attachment_filenames},
880
id=str(uuid.uuid4()),
webui/components/messages/process-group/process-group.css
+2
-2
@@ -634,7 +634,7 @@
634
-webkit-overflow-scrolling: touch;
635
scrollbar-width: thin;
636
scrollbar-color: rgba(255,255,255,0.2) transparent;
637
- overscroll-behavior: contain;
637
+ overscroll-behavior-x: contain;
638
}
639
640
.process-step-detail-content .terminal-output::-webkit-scrollbar {
@@ -777,7 +777,7 @@
777
}
778
779
.process-step-detail-content {
780
- margin-left: 16px;
780
+ margin-left: 0;
781
}
782
}
783
webui/css/messages.css
+6
@@ -77,6 +77,12 @@
77
width: 100%;
78
}
79
80
+.message-user .message-user-heading {
81
+ margin-top: var(--spacing-sm);
82
+ font-size: var(--font-size-smaller);
83
+ color: var(--color-text-muted);
84
+}
85
+
86
.message-user .message-text {
87
display: inline-block;
88
padding: var(--spacing-sm);
webui/index.css
+8
-8
@@ -1029,7 +1029,7 @@ input:checked + .slider:before {
1029
gap: 6px;
1030
padding: var(--spacing-xs);
1031
}
1032
-
1032
+/*
1033
.preview-item,
1034
.attachment-item {
1035
width: 90px;
@@ -1047,24 +1047,24 @@ input:checked + .slider:before {
1047
width: 32px;
1048
height: 32px;
1049
margin-bottom: 4px;
1050
- }
1050
+ } */
1051
1052
- .preview-item.file-preview .filename,
1052
+ /* .preview-item.file-preview .filename,
1053
.attachment-item.file-type .file-title,
1054
.attachment-name,
1055
.filename {
1056
font-size: 0.65rem;
1057
-webkit-line-clamp: 1;
1058
- }
1058
+ } */
1059
1060
- .preview-item.file-preview .extension,
1060
+ /* .preview-item.file-preview .extension,
1061
.attachment-item.file-type .file-extension,
1062
.attachment-item.image-type .image-badge,
1063
.attachment-ext,
1064
.extension {
1065
font-size: 0.6rem;
1066
padding: 1px 4px;
1067
- }
1067
+ } */
1068
1069
/* .remove-attachment,
1070
.remove-image {
@@ -1074,11 +1074,11 @@ input:checked + .slider:before {
1074
right: 2px;
1075
} */
1076
1077
- .attachment-item.image-type .image-badge {
1077
+ /* .attachment-item.image-type .image-badge {
1078
bottom: 4px;
1079
font-size: 0.55rem;
1080
padding: 1px 6px;
1081
- }
1081
+ } */
1082
}
1083
1084
@media (max-width: 640px) {
webui/index.js
+1
-1
@@ -63,7 +63,7 @@ export async function sendMessage() {
63
const heading =
64
attachmentsWithUrls.length > 0
65
? "Uploading attachments..."
66
- : "User message";
66
+ : "";
67
68
// Render user message with attachments
69
setMessage(messageId, "user", heading, message, false, {
webui/js/messages.js
+54
-93
@@ -121,9 +121,8 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
121
let messageContainer = document.getElementById(`message-${id}`);
122
let processStepElement = document.getElementById(`process-step-${id}`);
123
124
- // For user/info messages, close current process group FIRST (start fresh for next interaction)
125
- // Info messages (like nudge) should also split groups to mark a new processing phase
126
- if (type === "user" || type === "info") {
124
+ // For user messages, close current process group FIRST (start fresh for next interaction)
125
+ if (type === "user") {
126
currentProcessGroup = null;
127
currentDelegationSteps = {}; // Clear delegation tracking
128
}
@@ -336,8 +335,8 @@ export function _drawMessage(
335
});
336
}
337
339
- // Ensure action buttons exist - pass content directly
340
- addActionButtonsToElement(bodyDiv, { contentRef: content });
338
+ // Ensure action buttons exist
339
+ // addActionButtonsToElement(bodyDiv);
340
adjustMarkdownRender(contentDiv);
341
342
} else {
@@ -361,8 +360,8 @@ export function _drawMessage(
360
361
spanElement.innerHTML = convertHTML(content);
362
364
- // Ensure action buttons exist - pass content directly
365
- addActionButtonsToElement(bodyDiv, { contentRef: content });
363
+ // Ensure action buttons exist
364
+ // addActionButtonsToElement(bodyDiv);
365
366
}
367
} else {
@@ -531,12 +530,6 @@ export function drawMessageUser(
530
messageDiv.className = "message message-user";
531
}
532
534
- // Remove heading element if it exists (user messages no longer show label per target design)
535
- let headingElement = messageDiv.querySelector(".msg-heading");
536
- if (headingElement) {
537
- headingElement.remove();
538
- }
539
-
533
// Handle content
534
let textDiv = messageDiv.querySelector(".message-text");
535
if (content && content.trim().length > 0) {
@@ -551,6 +544,7 @@ export function drawMessageUser(
544
textDiv.appendChild(spanElement);
545
}
546
spanElement.innerHTML = escapeHTML(content);
547
+ // addActionButtonsToElement(textDiv);
548
} else {
549
if (textDiv) textDiv.remove();
550
}
@@ -614,8 +608,18 @@ export function drawMessageUser(
608
if (attachmentsContainer) attachmentsContainer.remove();
609
}
610
617
- // Add action buttons below text and attachments (hover for pointer, always for touch - via CSS)
618
- addActionButtonsToElement(messageDiv, { contentRef: content });
611
+ // Render heading below message, if provided
612
+ let headingElement = messageDiv.querySelector(".message-user-heading");
613
+ if (heading && heading.trim() && heading.trim() !== "User message") {
614
+ if (!headingElement) {
615
+ headingElement = document.createElement("div");
616
+ headingElement.className = "message-user-heading shiny-text";
617
+ }
618
+ headingElement.textContent = heading;
619
+ messageDiv.appendChild(headingElement);
620
+ } else if (headingElement) {
621
+ headingElement.remove();
622
+ }
623
// The messageDiv is already appended or updated, no need to append again
624
}
625
@@ -853,7 +857,7 @@ export function drawMessageError(
857
messageDiv.appendChild(errorGroup);
858
859
// Check detail mode and expand if needed
856
- const detailMode = preferencesStore.detailMode || "current";
860
+ const detailMode = window.Alpine?.store("preferences")?.detailMode || "current";
861
if (detailMode === "current" || detailMode === "expanded") {
862
errorGroup.classList.add("expanded");
863
}
@@ -1427,27 +1431,19 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1431
// Get step info from heading (single source of truth: backend)
1432
const title = getStepTitle(heading, kvps, type);
1433
1430
- // Determine if this new step should be expanded
1431
- const detailMode = preferencesStore.detailMode;
1432
- let shouldExpand = false;
1434
+ // Check if step should be expanded
1435
+ const isActiveStep = !isGroupCompleted && group.id === activeProcessGroupId;
1436
+ const isStepExpanded = processGroupStore.expandStep(groupId, id, isActiveStep);
1437
1434
- if (detailMode === "expanded") {
1435
- shouldExpand = true;
1436
- } else if (detailMode === "current" && !isGroupCompleted) {
1437
- // In "current" mode: expand new step, delay-collapse previous
1438
- shouldExpand = true;
1439
-
1440
- // Find the currently expanded step (the previous one) to collapse after delay
1441
- const previousExpandedStep = stepsContainer.querySelector(".process-step.step-expanded");
1442
- if (previousExpandedStep) {
1443
- setTimeout(() => {
1444
- previousExpandedStep.classList.remove("step-expanded");
1445
- }, 2000);
1446
- }
1438
+ // In "current" mode, collapse all other steps
1439
+ const detailMode = preferencesStore.detailMode;
1440
+ if (detailMode === "current" && isStepExpanded) {
1441
+ document.querySelectorAll(".process-step.step-expanded").forEach(s => {
1442
+ s.classList.remove("step-expanded");
1443
+ });
1444
}
1448
- // In "collapsed" mode: shouldExpand stays false
1445
1450
- if (shouldExpand) {
1446
+ if (isStepExpanded) {
1447
step.classList.add("step-expanded");
1448
}
1449
@@ -1507,9 +1503,9 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1503
toolName: toolNameToUse
1504
};
1505
1510
- // Add step action buttons (view details, copy, speak) for full modal view
1511
- const stepActionBtns = createStepActionButtons(step);
1512
- detail.appendChild(stepActionBtns);
1506
+ // Add "View Details" button for full modal view (reads fresh data from step._stepData)
1507
+ const viewDetailsBtn = createViewDetailsButton(step);
1508
+ detail.appendChild(viewDetailsBtn);
1509
1510
step.appendChild(detail);
1511
@@ -1591,15 +1587,9 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
1587
let skipFullRender = false;
1588
1589
if (detailContent) {
1594
- // Capture scroll state before re-render
1590
+ // Capture scroll state before re-render (uses existing Scroller pattern)
1591
const terminal = detailContent.querySelector(".terminal-output");
1596
- let wasAtBottom = true; // Default to true for new terminals
1597
- let previousScrollTop = 0;
1598
- if (terminal) {
1599
- const distanceFromBottom = terminal.scrollHeight - terminal.clientHeight - terminal.scrollTop;
1600
- wasAtBottom = distanceFromBottom <= 10;
1601
- previousScrollTop = terminal.scrollTop;
1602
- }
1592
+ const scroller = terminal ? new Scroller(terminal) : null;
1593
1594
// For browser, update image src incrementally to avoid flashing
1595
if (type === "browser" && kvps?.screenshot) {
@@ -1618,17 +1608,10 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
1608
if (!skipFullRender) {
1609
renderStepDetailContent(detailContent, content, kvps, type);
1610
1621
- // Restore scroll position after re-render
1611
+ // Re-apply scroll (stays at bottom if was at bottom)
1612
const newTerminal = detailContent.querySelector(".terminal-output");
1623
- if (newTerminal) {
1624
- if (wasAtBottom) {
1625
- // Pin to bottom if was at bottom
1626
- newTerminal.scrollTop = newTerminal.scrollHeight;
1627
- } else {
1628
- // Restore previous position (clamped to max)
1629
- const maxScroll = Math.max(newTerminal.scrollHeight - newTerminal.clientHeight, 0);
1630
- newTerminal.scrollTop = Math.min(previousScrollTop, maxScroll);
1631
- }
1613
+ if (newTerminal && scroller?.wasAtBottom) {
1614
+ newTerminal.scrollTop = newTerminal.scrollHeight;
1615
}
1616
}
1617
}
@@ -1657,20 +1640,17 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
1640
* Get a concise title for a process step
1641
*/
1642
function getStepTitle(heading, kvps, type) {
1660
- // Frontend-only: hide distracting leading session index like "[0] " in titles
1661
- const stripSessionPrefix = (s) => String(s || "").replace(/^\s*\[\d+\]\s*/, "");
1662
-
1643
// code_exe: show command when finished
1644
const showCommand = type === "code_exe" && kvps?.code &&
1645
/done_all|code_execution_tool/.test(heading || "");
1646
if (showCommand) {
1667
- return `${kvps.runtime || "bash"}> ${kvps.code.trim()}`;
1647
+ const s = kvps.session ?? kvps.Session;
1648
+ return `${s != null ? `[${s}] ` : ""}${kvps.runtime || "bash"}> ${kvps.code.trim()}`;
1649
}
1650
1651
// Try to get a meaningful title from heading or kvps
1652
if (heading && heading.trim()) {
1672
- const cleaned = cleanStepTitle(heading, 100);
1673
- return type === "code_exe" ? stripSessionPrefix(cleaned) : cleaned;
1653
+ return cleanStepTitle(heading, 100);
1654
}
1655
1656
// For warnings/errors without heading, use content preview as title
@@ -1977,37 +1957,26 @@ function renderThoughts(container, value) {
1957
}
1958
1959
/**
1980
- * Create step action buttons (view details, copy, speak) using unified action buttons
1960
+ * Create "View Details" button for opening step detail modal
1961
* @param {HTMLElement} stepElement - The step DOM element containing _stepData property
1962
*/
1983
-function createStepActionButtons(stepElement) {
1963
+function createViewDetailsButton(stepElement) {
1964
const btnContainer = document.createElement("div");
1965
btnContainer.classList.add("step-detail-actions");
1966
1987
- // Use unified action buttons with step-specific options
1988
- addActionButtonsToElement(btnContainer, {
1989
- contentRef: () => {
1990
- // Get text content from step data at action time
1991
- const data = stepElement._stepData || {};
1992
- const parts = [];
1993
- if (data.heading) parts.push(data.heading);
1994
- if (data.content) parts.push(data.content);
1995
- if (data.kvps) {
1996
- for (const [key, value] of Object.entries(data.kvps)) {
1997
- if (key === "reasoning" || key === "finished" || key === "attachments") continue;
1998
- const valStr = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
1999
- parts.push(`${key}: ${valStr}`);
2000
- }
2001
- }
2002
- return parts.join("\n\n");
2003
- },
2004
- onViewDetails: () => {
2005
- // Read fresh data from the step element at click time
2006
- const freshData = stepElement._stepData || {};
2007
- stepDetailStore.showStepDetail(freshData);
2008
- }
1967
+ const btn = document.createElement("button");
1968
+ btn.classList.add("btn", "text-button");
1969
+ btn.innerHTML = '<span class="material-symbols-outlined">open_in_full</span> View Details';
1970
+ btn.title = "Open full step details in modal";
1971
+
1972
+ btn.addEventListener("click", (e) => {
1973
+ e.stopPropagation();
1974
+ // Read fresh data from the step element at click time
1975
+ const freshData = stepElement._stepData || {};
1976
+ stepDetailStore.showStepDetail(freshData);
1977
});
1978
1979
+ btnContainer.appendChild(btn);
1980
return btnContainer;
1981
}
1982
@@ -2190,14 +2159,6 @@ function markProcessGroupComplete(group, responseTitle) {
2159
// Add completed class to group
2160
group.classList.add("process-group-completed");
2161
2193
- // Collapse the last expanded step when processing is done (in "current" mode)
2194
- const detailMode = preferencesStore.detailMode;
2195
- if (detailMode === "current") {
2196
- const expandedStep = group.querySelector(".process-step.step-expanded");
2197
- if (expandedStep) {
2198
- expandedStep.classList.remove("step-expanded");
2199
- }
2200
- }
2162
2163
// Calculate final duration from backend data (sum of all step durations)
2164
const steps = group.querySelectorAll(".process-step");