fix: take durationms from backend
duration and timestamp persist log and process groups cleanup
3clyp50 committed
Jan 2, 2026 at 17:40 UTC
5844e58b83bbc37cc3d4734067b2a630b5299686
7 files changed
+90
-113
python/helpers/log.py
+1
-6
@@ -138,12 +138,7 @@ class LogItem:
138
139
def __post_init__(self):
140
self.guid = self.log.guid
141
- self.timestamp = time.time() # Record creation time
142
- # Capture agent number from context if available
143
- if self.log.context and self.log.context.streaming_agent:
144
- self.agent_number = self.log.context.streaming_agent.number
145
- else:
146
- self.agent_number = 0 # Default to main agent
141
+ self.timestamp = time.time()
142
143
def update(
144
self,
python/helpers/persist_chat.py
+13
-11
@@ -262,17 +262,19 @@ def _deserialize_log(data: dict[str, Any]) -> "Log":
262
# Deserialize the list of LogItem objects
263
i = 0
264
for item_data in data.get("logs", []):
265
- log.logs.append(
266
- LogItem(
267
- log=log, # restore the log reference
268
- no=i, # item_data["no"],
269
- type=item_data["type"],
270
- heading=item_data.get("heading", ""),
271
- content=item_data.get("content", ""),
272
- kvps=OrderedDict(item_data["kvps"]) if item_data["kvps"] else None,
273
- temp=item_data.get("temp", False),
274
- )
275
- )
265
+ log.logs.append(LogItem(
266
+ log=log, # restore the log reference
267
+ no=i, # item_data["no"],
268
+ type=item_data["type"],
269
+ heading=item_data.get("heading", ""),
270
+ content=item_data.get("content", ""),
271
+ kvps=OrderedDict(item_data["kvps"]) if item_data["kvps"] else None,
272
+ temp=item_data.get("temp", False),
273
+ # Pass metrics directly to constructor
274
+ timestamp=item_data.get("timestamp", 0.0),
275
+ duration_ms=item_data.get("duration_ms"),
276
+ agent_number=item_data.get("agent_number", 0),
277
+ ))
278
log.updates.append(i)
279
i += 1
280
webui/components/messages/process-group/process-group.css
-7
@@ -669,13 +669,6 @@
669
height: 0;
670
}
671
672
-
673
- }
674
- #chat-input::-webkit-scrollbar { width: 6px; height: 6px; }
675
- #chat-input::-webkit-scrollbar-track { background: transparent; margin: 4px 0; border-radius: 6px; }
676
- #chat-input::-webkit-scrollbar-thumb { background-color: rgba(155,155,155,0.5); border-radius: 6px; -webkit-transition: background-color 0.2s ease; transition: background-color 0.2s ease; }
677
- #chat-input::-webkit-scrollbar-thumb:hover { background-color: rgba(155,155,155,0.7); }
678
-
672
/* Light mode terminal */
673
.light-mode .process-step-detail-content .terminal-output {
674
background: rgba(0, 0, 0, 0.05);
webui/css/messages.css
+2
@@ -96,6 +96,8 @@
96
97
.message-user .message-text pre {
98
font-family: var(--font-family-main);
99
+ text-align: right;
100
+ margin-left: 25%;
101
}
102
103
.message-ai {
webui/index.js
-1
@@ -10,7 +10,6 @@ import { store as inputStore } from "/components/chat/input/input-store.js";
10
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
11
import { store as tasksStore } from "/components/sidebar/tasks/tasks-store.js";
12
import { store as chatTopStore } from "/components/chat/top-section/chat-top-store.js";
13
-import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js";
13
14
globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
15
webui/js/messages.js
+57
-88
@@ -6,6 +6,7 @@ import { store as attachmentsStore } from "/components/chat/attachments/attachme
6
import { addActionButtonsToElement } from "/components/messages/action-buttons/simple-action-buttons.js";
7
import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js";
8
import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
9
+import { formatDuration } from "./time-utils.js";
10
11
const chatHistory = document.getElementById("chat-history");
12
@@ -13,11 +14,11 @@ let messageGroup = null;
14
let currentProcessGroup = null; // Track current process group for collapsible UI
15
16
// Process types that should be grouped into collapsible sections
16
-const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'info', 'hint', 'util', 'warning'];
17
+const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'progress', 'info', 'hint', 'util', 'warning'];
18
// Main types that should always be visible (not collapsed)
19
const MAIN_TYPES = ['user', 'response', 'error', 'rate_limit'];
20
20
-export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, /* tokensIn = 0, tokensOut = 0, */ agentNumber = 0) {
21
+export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
22
// Check if this is a process type message
23
const isProcessType = PROCESS_TYPES.includes(type);
24
const isMainType = MAIN_TYPES.includes(type);
@@ -36,7 +37,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
37
if (isProcessType) {
38
if (processStepElement) {
39
// Update existing process step
39
- updateProcessStep(processStepElement, id, type, heading, content, kvps, durationMs /*, tokensIn, tokensOut */);
40
+ updateProcessStep(processStepElement, id, type, heading, content, kvps, durationMs);
41
return processStepElement;
42
}
43
@@ -47,7 +48,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
48
}
49
50
// Add step to current process group
50
- processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp, durationMs /*, tokensIn, tokensOut */);
51
+ processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp, durationMs);
52
return processStepElement;
53
}
54
@@ -55,7 +56,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
56
// agentNumber: 0 = main agent, 1+ = subordinate agents
57
if (type === "response" && agentNumber !== 0) {
58
if (processStepElement) {
58
- updateProcessStep(processStepElement, id, "agent", heading, content, kvps, durationMs /*, tokensIn, tokensOut */);
59
+ updateProcessStep(processStepElement, id, "agent", heading, content, kvps, durationMs);
60
return processStepElement;
61
}
62
@@ -66,7 +67,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
67
}
68
69
// Add subordinate response as a step (type "agent" for appropriate styling)
69
- processStepElement = addProcessStep(currentProcessGroup, id, "agent", heading, content, kvps, timestamp, durationMs /*, tokensIn, tokensOut */);
70
+ processStepElement = addProcessStep(currentProcessGroup, id, "agent", heading, content, kvps, timestamp, durationMs);
71
return processStepElement;
72
}
73
@@ -1140,7 +1141,6 @@ function createProcessGroup(id) {
1141
<span class="metric-time" title="Start time"><span class="material-symbols-outlined">schedule</span><span class="metric-value">--:--</span></span>
1142
<span class="metric-steps" title="Steps"><span class="material-symbols-outlined">list_alt</span><span class="metric-value">0</span></span>
1143
<span class="metric-duration" title="Duration"><span class="material-symbols-outlined">timer</span><span class="metric-value">0s</span></span>
1143
- <!-- <span class="metric-tokens" title="Tokens"><span class="material-symbols-outlined">data_object</span><span class="metric-value">--</span></span> -->
1144
</span>
1145
`;
1146
@@ -1170,7 +1170,7 @@ function createProcessGroup(id) {
1170
/**
1171
* Add a step to a process group
1172
*/
1173
-function addProcessStep(group, id, type, heading, content, kvps, timestamp = null, durationMs = null /*, tokensIn = 0, tokensOut = 0 */) {
1173
+function addProcessStep(group, id, type, heading, content, kvps, timestamp = null, durationMs = null) {
1174
const groupId = group.getAttribute("data-group-id");
1175
const stepsContainer = group.querySelector(".process-steps");
1176
const isGroupCompleted = group.classList.contains("process-group-completed");
@@ -1181,8 +1181,6 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1181
step.classList.add("process-step");
1182
step.setAttribute("data-type", type);
1183
step.setAttribute("data-step-id", id);
1184
- // step.setAttribute("data-tokens-in", tokensIn || 0);
1185
- // step.setAttribute("data-tokens-out", tokensOut || 0);
1184
1185
// Store timestamp for duration calculation
1186
if (timestamp) {
@@ -1199,6 +1197,11 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1197
}
1198
}
1199
1200
+ // Store duration from backend (used for final duration calculation)
1201
+ if (durationMs != null) {
1202
+ step.setAttribute("data-duration-ms", durationMs);
1203
+ }
1204
+
1205
// Add message-util class for utility/info types (controlled by showUtils preference)
1206
if (type === "util" || type === "info" || type === "hint") {
1207
step.classList.add("message-util");
@@ -1272,7 +1275,7 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1275
/**
1276
* Update an existing process step
1277
*/
1275
-function updateProcessStep(stepElement, id, type, heading, content, kvps, durationMs = null /*, tokensIn = 0, tokensOut = 0 */) {
1278
+function updateProcessStep(stepElement, id, type, heading, content, kvps, durationMs = null) {
1279
// Update title
1280
const titleEl = stepElement.querySelector(".step-title");
1281
if (titleEl) {
@@ -1280,13 +1283,10 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
1283
titleEl.textContent = title;
1284
}
1285
1283
- // Update token data (use the latest values)
1284
- // if (tokensIn > 0) {
1285
- // stepElement.setAttribute("data-tokens-in", tokensIn);
1286
- // }
1287
- // if (tokensOut > 0) {
1288
- // stepElement.setAttribute("data-tokens-out", tokensOut);
1289
- // }
1286
+ // Update duration from backend
1287
+ if (durationMs != null) {
1288
+ stepElement.setAttribute("data-duration-ms", durationMs);
1289
+ }
1290
1291
// Update detail content
1292
const detailContent = stepElement.querySelector(".process-step-detail-content");
@@ -1590,8 +1590,15 @@ function updateProcessGroupHeader(group) {
1590
const metricsEl = group.querySelector(".group-metrics");
1591
const isCompleted = group.classList.contains("process-group-completed");
1592
1593
+ // If completed, only remove active badges and exit early (don't update metrics)
1594
+ if (isCompleted) {
1595
+ const activeBadges = group.querySelectorAll(".status-badge.status-active");
1596
+ activeBadges.forEach(badge => badge.classList.remove("status-active"));
1597
+ return;
1598
+ }
1599
+
1600
// Update group title with the latest agent step heading
1594
- if (titleEl && !isCompleted) {
1601
+ if (titleEl) {
1602
// Find the last "agent" type step
1603
const agentSteps = Array.from(steps).filter(step => step.getAttribute("data-type") === "agent");
1604
if (agentSteps.length > 0) {
@@ -1622,44 +1629,28 @@ function updateProcessGroupHeader(group) {
1629
timeMetricEl.textContent = `${hours}:${minutes}`;
1630
}
1631
1625
- // Update duration metric (elapsed time since start) - but only if not completed
1632
+ // Update duration metric
1633
const durationMetricEl = metricsEl?.querySelector(".metric-duration .metric-value");
1627
- if (durationMetricEl && startTimestamp && !isCompleted) {
1628
- const startMs = parseFloat(startTimestamp) * 1000;
1629
- const elapsedMs = Date.now() - startMs;
1630
- if (elapsedMs < 60000) {
1631
- durationMetricEl.textContent = `${Math.round(elapsedMs / 1000)}s`;
1632
- } else {
1633
- const mins = Math.floor(elapsedMs / 60000);
1634
- const secs = Math.round((elapsedMs % 60000) / 1000);
1635
- durationMetricEl.textContent = `${mins}m${secs}s`;
1634
+ if (durationMetricEl && steps.length > 0) {
1635
+ // Calculate accumulated duration from backend data
1636
+ let accumulatedMs = 0;
1637
+ steps.forEach(step => {
1638
+ accumulatedMs += parseInt(step.getAttribute("data-duration-ms") || "0", 10);
1639
+ });
1640
+
1641
+ // Check if last step is still in progress (no duration_ms set yet)
1642
+ const lastStep = steps[steps.length - 1];
1643
+ const lastStepDuration = lastStep.getAttribute("data-duration-ms");
1644
+ const lastStepTimestamp = lastStep.getAttribute("data-timestamp");
1645
+
1646
+ if (lastStepDuration == null && lastStepTimestamp) {
1647
+ // Last step is in progress - add live elapsed time for this step only
1648
+ const lastStepStartMs = parseFloat(lastStepTimestamp) * 1000;
1649
+ const liveElapsedMs = Math.max(0, Date.now() - lastStepStartMs);
1650
+ accumulatedMs += liveElapsedMs;
1651
}
1637
- }
1638
-
1639
- // Update tokens metric (aggregate from all steps)
1640
- // const tokensMetricEl = metricsEl?.querySelector(".metric-tokens .metric-value");
1641
- // if (tokensMetricEl) {
1642
- // let totalTokensIn = 0;
1643
- // let totalTokensOut = 0;
1644
- // steps.forEach(step => {
1645
- // totalTokensIn += parseInt(step.getAttribute("data-tokens-in") || 0, 10);
1646
- // totalTokensOut += parseInt(step.getAttribute("data-tokens-out") || 0, 10);
1647
- // });
1648
- // const totalTokens = totalTokensIn + totalTokensOut;
1649
- // if (totalTokens > 0) {
1650
- // // Format as compact notation (e.g., 20k/3k for input/output)
1651
- // tokensMetricEl.textContent = formatTokenCount(totalTokensIn, totalTokensOut);
1652
- // } else {
1653
- // tokensMetricEl.textContent = "--";
1654
- // }
1655
- // }
1656
-
1657
- // Once a group is completed, never re-enable any loading spinners (status-active).
1658
- // This prevents late util/tool messages from making a completed group look "running".
1659
- if (isCompleted) {
1660
- const activeBadges = group.querySelectorAll(".status-badge.status-active");
1661
- activeBadges.forEach(badge => badge.classList.remove("status-active"));
1662
- return;
1652
+
1653
+ durationMetricEl.textContent = formatDuration(accumulatedMs);
1654
}
1655
1656
if (steps.length > 0) {
@@ -1741,20 +1732,19 @@ function markProcessGroupComplete(group, responseTitle) {
1732
// Add completed class to group
1733
group.classList.add("process-group-completed");
1734
1744
- // Update duration to final value
1735
+ // Calculate final duration from backend data (sum of all step durations)
1736
+ const steps = group.querySelectorAll(".process-step");
1737
+ let totalDurationMs = 0;
1738
+ steps.forEach(step => {
1739
+ const durationMs = parseInt(step.getAttribute("data-duration-ms") || "0", 10);
1740
+ totalDurationMs += durationMs;
1741
+ });
1742
+
1743
+ // Update duration metric with final value from backend
1744
const metricsEl = group.querySelector(".group-metrics");
1745
const durationMetricEl = metricsEl?.querySelector(".metric-duration .metric-value");
1747
- const startTimestamp = group.getAttribute("data-start-timestamp");
1748
- if (durationMetricEl && startTimestamp) {
1749
- const startMs = parseFloat(startTimestamp) * 1000;
1750
- const elapsedMs = Date.now() - startMs;
1751
- if (elapsedMs < 60000) {
1752
- durationMetricEl.textContent = `${Math.round(elapsedMs / 1000)}s`;
1753
- } else {
1754
- const mins = Math.floor(elapsedMs / 60000);
1755
- const secs = Math.round((elapsedMs % 60000) / 1000);
1756
- durationMetricEl.textContent = `${mins}m${secs}s`;
1757
- }
1746
+ if (durationMetricEl && totalDurationMs > 0) {
1747
+ durationMetricEl.textContent = formatDuration(totalDurationMs);
1748
}
1749
}
1750
@@ -1766,27 +1756,6 @@ export function resetProcessGroups() {
1756
messageGroup = null;
1757
}
1758
1769
-/**
1770
- * Format token counts in compact notation (e.g., "12k/3k" for input/output)
1771
- * (Currently disabled - token tracking not implemented)
1772
- */
1773
-// function formatTokenCount(tokensIn, tokensOut) {
1774
-// const formatCompact = (n) => {
1775
-// if (n >= 1000000) return `${(n / 1000000).toFixed(1)}m`;
1776
-// if (n >= 1000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k`;
1777
-// return n.toString();
1778
-// };
1779
-//
1780
-// if (tokensIn > 0 && tokensOut > 0) {
1781
-// return `${formatCompact(tokensIn)}/${formatCompact(tokensOut)}`;
1782
-// } else if (tokensIn > 0) {
1783
-// return `${formatCompact(tokensIn)}↓`;
1784
-// } else if (tokensOut > 0) {
1785
-// return `${formatCompact(tokensOut)}↑`;
1786
-// }
1787
-// return "--";
1788
-// }
1789
-
1759
/**
1760
* Format Unix timestamp as date-time string (YYYY-MM-DD HH:MM:SS)
1761
*/
webui/js/time-utils.js
+17
@@ -69,3 +69,20 @@ export function formatDateTime(utcIsoString, format = 'full') {
69
export function getUserTimezone() {
70
return Intl.DateTimeFormat().resolvedOptions().timeZone;
71
}
72
+
73
+/**
74
+ * Format a duration in milliseconds to a human-readable string
75
+ * @param {number} durationMs - Duration in milliseconds
76
+ * @returns {string} Formatted duration (e.g., '45s', '2m30s')
77
+ */
78
+export function formatDuration(durationMs) {
79
+ if (durationMs == null || durationMs < 0) return '0s';
80
+
81
+ if (durationMs < 60000) {
82
+ return `${Math.round(durationMs / 1000)}s`;
83
+ }
84
+
85
+ const mins = Math.floor(durationMs / 60000);
86
+ const secs = Math.round((durationMs % 60000) / 1000);
87
+ return `${mins}m${secs}s`;
88
+}