remove bg-colors from messages; logItem updates

- backend-driven step duration with live counter - Add duration_ms field to LogItem (python/helpers/log.py) - Pass duration_ms through API to frontend - Live timer for in-progress steps, duration_ms final value for completed

Alessandro committed Dec 15, 2025 at 18:37 UTC 8181a8268a67cb10e28da04477e100e7285f7ec3
7 files changed +981 -224
python/helpers/log.py
+30
@@ -133,6 +133,9 @@ class LogItem:
133 id: Optional[str] = None # Add id field
134 guid: str = ""
135 timestamp: float = 0.0 # Unix timestamp in seconds
136 + duration_ms: Optional[int] = None # Duration until next step (set by Log.log)
137 + tokens_in: int = 0 # Input tokens consumed
138 + tokens_out: int = 0 # Output tokens generated
139
140 def __post_init__(self):
141 self.guid = self.log.guid
@@ -146,6 +149,8 @@ class LogItem:
149 kvps: dict | None = None,
150 temp: bool | None = None,
151 update_progress: ProgressUpdate | None = None,
152 + tokens_in: int | None = None,
153 + tokens_out: int | None = None,
154 **kwargs,
155 ):
156 if self.guid == self.log.guid:
@@ -157,9 +162,18 @@ class LogItem:
162 kvps=kvps,
163 temp=temp,
164 update_progress=update_progress,
165 + tokens_in=tokens_in,
166 + tokens_out=tokens_out,
167 **kwargs,
168 )
169
170 + def add_tokens(self, tokens_in: int = 0, tokens_out: int = 0):
171 + """Add tokens to this log item (accumulative)."""
172 + if self.guid == self.log.guid:
173 + self.tokens_in += tokens_in
174 + self.tokens_out += tokens_out
175 + self.log.updates += [self.no]
176 +
177 def stream(
178 self,
179 heading: str | None = None,
@@ -185,6 +199,9 @@ class LogItem:
199 "temp": self.temp,
200 "kvps": self.kvps,
201 "timestamp": self.timestamp, # Unix timestamp in seconds
202 + "duration_ms": self.duration_ms, # Duration until next step
203 + "tokens_in": self.tokens_in, # Input tokens
204 + "tokens_out": self.tokens_out, # Output tokens
205 }
206
207
@@ -215,6 +232,11 @@ class Log:
232 no=len(self.logs),
233 type=type,
234 )
235 + # Set duration on previous item and mark it as updated
236 + if self.logs:
237 + prev = self.logs[-1]
238 + prev.duration_ms = int((item.timestamp - prev.timestamp) * 1000)
239 + self.updates += [prev.no]
240 self.logs.append(item)
241
242 # and update it (to have just one implementation)
@@ -241,6 +263,8 @@ class Log:
263 temp: bool | None = None,
264 update_progress: ProgressUpdate | None = None,
265 id: Optional[str] = None,
266 + tokens_in: int | None = None,
267 + tokens_out: int | None = None,
268 **kwargs,
269 ):
270 item = self.logs[no]
@@ -257,6 +281,12 @@ class Log:
281 if update_progress is not None:
282 item.update_progress = update_progress
283
284 + if tokens_in is not None:
285 + item.tokens_in = tokens_in
286 +
287 + if tokens_out is not None:
288 + item.tokens_out = tokens_out
289 +
290
291 # adjust all content before processing
292 if heading is not None:
webui/components/messages/process-group/process-group-store.js
+80 -8
@@ -78,10 +78,10 @@ const model = {
78 this._persist();
79 },
80
81 - // Get icon for step type
81 + // Get icon for step type (Material Symbols)
82 getStepIcon(type) {
83 const icons = {
84 - 'agent': 'psychology',
84 + 'agent': 'neurology',
85 'tool': 'build',
86 'code_exe': 'terminal',
87 'browser': 'language',
@@ -89,23 +89,95 @@ const model = {
89 'hint': 'lightbulb',
90 'util': 'settings',
91 'warning': 'warning',
92 - 'error': 'error'
92 + 'error': 'error',
93 + 'mcp': 'api',
94 + 'memory': 'memory',
95 + 'done': 'check_circle',
96 + 'subagent': 'group'
97 };
98 return icons[type] || 'circle';
99 },
100
97 - // Get label for step type
101 + // Get 3-letter status code for step type
102 + getStepCode(type) {
103 + const codes = {
104 + 'agent': 'GEN',
105 + 'tool': 'MCP',
106 + 'code_exe': 'EXE',
107 + 'browser': 'EXE',
108 + 'info': 'INF',
109 + 'hint': 'INF',
110 + 'util': 'UTL',
111 + 'warning': 'WRN',
112 + 'error': 'ERR',
113 + 'mcp': 'MCP',
114 + 'memory': 'MEM',
115 + 'done': 'END',
116 + 'response': 'END',
117 + 'subagent': 'SUB'
118 + };
119 + return codes[type] || 'GEN';
120 + },
121 +
122 + // Get color class for status code
123 + getStatusColorClass(type) {
124 + const colors = {
125 + 'agent': 'status-gen',
126 + 'tool': 'status-mcp',
127 + 'code_exe': 'status-exe',
128 + 'browser': 'status-exe',
129 + 'info': 'status-inf',
130 + 'hint': 'status-inf',
131 + 'util': 'status-utl',
132 + 'warning': 'status-wrn',
133 + 'error': 'status-err',
134 + 'mcp': 'status-mcp',
135 + 'memory': 'status-mem',
136 + 'done': 'status-end',
137 + 'response': 'status-end',
138 + 'subagent': 'status-sub'
139 + };
140 + return colors[type] || 'status-gen';
141 + },
142 +
143 + // Get icon for badge display (Material Symbols)
144 + getStepBadgeIcon(type) {
145 + const icons = {
146 + 'agent': 'public', // Globe for GEN (network intelligence)
147 + 'tool': 'build', // Wrench for MCP
148 + 'code_exe': 'terminal', // Terminal for EXE
149 + 'browser': 'language', // Globe for browser
150 + 'info': 'info',
151 + 'hint': 'lightbulb',
152 + 'util': 'settings',
153 + 'warning': 'warning',
154 + 'error': 'error',
155 + 'mcp': 'build', // Wrench for MCP
156 + 'memory': 'memory',
157 + 'done': 'check_circle',
158 + 'response': 'check_circle',
159 + 'subagent': 'group'
160 + };
161 + return icons[type] || 'circle';
162 + },
163 +
164 + // Get label for step type (longer description)
165 getStepLabel(type) {
166 const labels = {
100 - 'agent': 'Thinking',
101 - 'tool': 'Tool',
102 - 'code_exe': 'Code',
167 + 'agent': 'Generating',
168 + 'tool': 'MCP Call',
169 + 'code_exe': 'Executing',
170 'browser': 'Browser',
171 'info': 'Info',
172 'hint': 'Hint',
173 'util': 'Utility',
174 'warning': 'Warning',
108 - 'error': 'Error'
175 + 'error': 'Error',
176 + 'mcp': 'MCP',
177 + 'memory': 'Memory',
178 + 'done': 'Done',
179 + 'response': 'Response',
180 + 'subagent': 'Subagent'
181 };
182 return labels[type] || 'Process';
183 },
webui/components/messages/process-group/process-group.css
+412 -89
@@ -3,11 +3,8 @@
3 flex-direction: column;
4 position: relative;
5 z-index: 1;
6 - margin: var(--spacing-sm) 0;
7 - padding: var(--spacing-sm) var(--spacing-md);
8 - border-radius: var(--border-radius);
9 - background: #1f3c1e;
10 - border: 1px solid rgba(255, 255, 255, 0.06);
6 + margin: var(--spacing-xs) 0;
7 + padding: var(--spacing-xs) 0;
8 min-width: 200px;
9 max-width: 100%;
10 box-sizing: border-box;
@@ -30,9 +27,6 @@
27 .message-container.has-process-group {
28 display: inline-flex;
29 flex-direction: column;
33 - background: #1f3c1e;
34 - border-radius: var(--border-radius);
35 - border: 1px solid rgba(255, 255, 255, 0.06);
30 padding: 0;
31 overflow: hidden;
32 min-width: 200px;
@@ -40,16 +34,11 @@
34 }
35
36 .message-container.has-process-group > .message {
43 - border-radius: 0 0 var(--border-radius) var(--border-radius);
37 border: none;
38 background: transparent;
39 margin: 0;
40 }
41
49 -.process-group:hover {
50 - border-color: rgba(255, 255, 255, 0.10);
51 -}
52 -
42 /* Process Group Header */
43 .process-group-header {
44 display: flex;
@@ -58,8 +47,8 @@
47 cursor: pointer;
48 user-select: none;
49 transition: opacity 0.15s ease;
61 - min-height: 24px;
62 - gap: var(--spacing-xs);
50 + min-height: 22px;
51 + gap: 6px;
52 white-space: nowrap;
53 }
54
@@ -67,52 +56,240 @@
56 opacity: 0.85;
57 }
58
59 +/* Expand/collapse triangle - CSS-only (no Material Icons) */
60 .process-group-header .expand-icon {
71 - font-size: 1rem;
72 - color: var(--color-text);
73 - transition: transform 0.2s ease;
61 + display: inline-block;
62 + width: 0;
63 + height: 0;
64 + border-style: solid;
65 + border-width: 4px 0 4px 6px;
66 + border-color: transparent transparent transparent var(--color-text);
67 opacity: 0.5;
68 + transition: transform 0.2s ease, opacity 0.15s ease;
69 flex-shrink: 0;
70 + font-size: 0; /* Hide any text content */
71 + margin-right: 2px;
72 +}
73 +
74 +.process-group-header:hover .expand-icon {
75 + opacity: 0.8;
76 }
77
78 .process-group.expanded .process-group-header .expand-icon {
79 transform: rotate(90deg);
80 }
81
82 +/* Group icon removed - using status badges */
83 .process-group-header .group-icon {
83 - font-size: 0.95rem;
84 - color: var(--color-primary);
85 - opacity: 0.7;
86 - flex-shrink: 0;
84 + display: none;
85 }
86
87 +/* Group title - prominent display */
88 .process-group-header .group-title {
90 - flex: 1;
89 + flex: 0 1 auto;
90 font-size: var(--font-size-smaller);
92 - font-weight: 400;
91 + font-weight: 500;
92 color: var(--color-text);
94 - opacity: 0.7;
93 + opacity: 0.9;
94 white-space: nowrap;
95 overflow: hidden;
96 text-overflow: ellipsis;
97 + max-width: 350px;
98 }
99
100 +/* Step count badge */
101 .process-group-header .step-count {
101 - font-size: 0.7rem;
102 + font-size: 0.65rem;
103 color: var(--color-text);
103 - opacity: 0.6;
104 + opacity: 0.5;
105 flex-shrink: 0;
105 - background-color: rgba(255, 255, 255, 0.06);
106 padding: 1px 6px;
107 - border-radius: 8px;
107 + font-family: var(--font-family-code);
108 +}
109 +
110 +/* ===========================================
111 + 3-Letter Status Code Badges
112 + =========================================== */
113 +
114 +/* Base status badge */
115 +.status-badge {
116 + display: inline-flex;
117 + align-items: center;
118 + gap: 3px;
119 + padding: 2px 6px;
120 + border-radius: 3px;
121 + font-size: 0.65rem;
122 + font-weight: 600;
123 + font-family: var(--font-family-code);
124 + text-transform: uppercase;
125 + letter-spacing: 0.5px;
126 + flex-shrink: 0;
127 + line-height: 1;
128 +}
129 +
130 +/* Status badge with icon */
131 +.status-badge .material-symbols-outlined {
132 + font-size: 0.8rem;
133 + line-height: 1;
134 +}
135 +
136 +/* Badge icon styling */
137 +.status-badge .badge-icon {
138 + font-size: 0.7rem;
139 + margin-right: 2px;
140 + opacity: 0.9;
141 +}
142 +
143 +/* GEN - Generating (blue/cyan) */
144 +.status-gen {
145 + background-color: rgba(56, 189, 248, 0.15);
146 + color: #38bdf8;
147 + border: 1px solid rgba(56, 189, 248, 0.3);
148 +}
149 +
150 +/* EXE - Executing (magenta/purple) */
151 +.status-exe {
152 + background-color: rgba(186, 104, 200, 0.15);
153 + color: #ba68c8;
154 + border: 1px solid rgba(186, 104, 200, 0.3);
155 +}
156 +
157 +/* MCP - MCP Tool Call (amber/yellow) */
158 +.status-mcp {
159 + background-color: rgba(251, 191, 36, 0.15);
160 + color: #fbbf24;
161 + border: 1px solid rgba(251, 191, 36, 0.3);
162 +}
163 +
164 +/* MEM - Memory (blue) */
165 +.status-mem {
166 + background-color: rgba(96, 165, 250, 0.15);
167 + color: #60a5fa;
168 + border: 1px solid rgba(96, 165, 250, 0.3);
169 +}
170 +
171 +/* END - Done (green) */
172 +.status-end {
173 + background-color: rgba(34, 197, 94, 0.15);
174 + color: #22c55e;
175 + border: 1px solid rgba(34, 197, 94, 0.3);
176 +}
177 +
178 +/* ERR - Error (red) */
179 +.status-err {
180 + background-color: rgba(239, 68, 68, 0.15);
181 + color: #ef4444;
182 + border: 1px solid rgba(239, 68, 68, 0.3);
183 +}
184 +
185 +/* WRN - Warning (orange) */
186 +.status-wrn {
187 + background-color: rgba(249, 115, 22, 0.15);
188 + color: #f97316;
189 + border: 1px solid rgba(249, 115, 22, 0.3);
190 }
191
192 +/* SUB - Subagent (teal) */
193 +.status-sub {
194 + background-color: rgba(20, 184, 166, 0.15);
195 + color: #14b8a6;
196 + border: 1px solid rgba(20, 184, 166, 0.3);
197 +}
198 +
199 +/* INF - Info (gray) */
200 +.status-inf {
201 + background-color: rgba(148, 163, 184, 0.15);
202 + color: #94a3b8;
203 + border: 1px solid rgba(148, 163, 184, 0.3);
204 +}
205 +
206 +/* UTL - Utility (gray-blue) */
207 +.status-utl {
208 + background-color: rgba(100, 116, 139, 0.15);
209 + color: #64748b;
210 + border: 1px solid rgba(100, 116, 139, 0.3);
211 +}
212 +
213 +/* Animated spinner for active status (CSS-only, no Material Icons) */
214 +.status-badge.status-active::after {
215 + content: "";
216 + display: inline-block;
217 + width: 8px;
218 + height: 8px;
219 + border: 1.5px solid currentColor;
220 + border-top-color: transparent;
221 + border-radius: 50%;
222 + animation: spin 0.8s linear infinite;
223 + margin-left: 4px;
224 + flex-shrink: 0;
225 +}
226 +
227 +@keyframes spin {
228 + from { transform: rotate(0deg); }
229 + to { transform: rotate(360deg); }
230 +}
231 +
232 +/* Don't show spinner pseudo-element on END/ERR (they have their own indicators) */
233 +.status-badge.status-end::after,
234 +.status-badge.status-err::after {
235 + display: none;
236 +}
237 +
238 +/* END status with checkmark icon (like sketch) - use ::before only */
239 +.status-end::before {
240 + content: "✓";
241 + margin-right: 2px;
242 + font-weight: bold;
243 +}
244 +
245 +/* Completed process group styling */
246 +.process-group-completed {
247 + opacity: 0.95;
248 +}
249 +
250 +.process-group-completed .group-status {
251 + font-weight: 700;
252 +}
253 +
254 +/* Subtle completion indicator on the expand icon */
255 +.process-group-completed .expand-icon {
256 + color: #22c55e;
257 +}
258 +
259 +/* Group status badge in header */
260 +.process-group-header .group-status {
261 + margin-left: var(--spacing-xs);
262 +}
263 +
264 +/* Metrics row */
265 +.process-group-header .group-metrics {
266 + display: flex;
267 + align-items: center;
268 + gap: var(--spacing-sm);
269 + margin-left: auto;
270 + font-size: 0.65rem;
271 + font-family: var(--font-family-code);
272 + color: var(--color-text);
273 + opacity: 0.6;
274 +}
275 +
276 +.process-group-header .group-metrics span[class^="metric-"] {
277 + display: inline-flex;
278 + align-items: center;
279 + gap: 2px;
280 +}
281 +
282 +.process-group-header .group-metrics .metric-value {
283 + opacity: 0.8;
284 +}
285 +
286 +/* Legacy timestamp/duration (for backwards compatibility) */
287 .process-group-header .group-timestamp {
288 font-size: 0.65rem;
289 color: rgba(255, 255, 255, 0.65);
290 flex-shrink: 0;
291 font-family: var(--font-family-code);
115 - margin-left: auto;
292 + display: none; /* Hidden, replaced by metrics */
293 }
294
295 .process-group-header .group-duration {
@@ -122,6 +299,7 @@
299 font-family: var(--font-family-code);
300 min-width: 40px;
301 text-align: right;
302 + display: none; /* Hidden, replaced by metrics */
303 }
304
305 /* Process Group Content - Animated expand/collapse */
@@ -147,8 +325,8 @@
325 .process-group.expanded .process-group-content {
326 grid-template-rows: 1fr;
327 opacity: 1;
150 - margin-top: var(--spacing-sm);
151 - padding-top: var(--spacing-sm);
328 + margin-top: var(--spacing-xs);
329 + padding-top: var(--spacing-xs);
330 border-top-color: rgba(255, 255, 255, 0.06);
331 }
332
@@ -164,15 +342,11 @@
342 .process-step {
343 display: flex;
344 flex-direction: column;
167 - padding: var(--spacing-xxs) var(--spacing-xs);
345 + padding: 1px var(--spacing-xs);
346 border-radius: 4px;
347 transition: background-color 0.15s ease;
348 }
349
172 -.process-step:hover {
173 - background-color: rgba(255, 255, 255, 0.03);
174 -}
175 -
350 /* Utility/Info/Hint steps have subtle background tint */
351 .process-step[data-type="util"],
352 .process-step[data-type="info"],
@@ -199,60 +373,45 @@
373 align-items: center;
374 cursor: pointer;
375 user-select: none;
202 - gap: var(--spacing-xs);
203 - min-height: 20px;
376 + gap: 4px;
377 + min-height: 18px;
378 }
379
380 +/* Step icon removed - using status badges instead */
381 .process-step-header .step-icon {
207 - font-size: 0.85rem;
208 - opacity: 0.6;
209 - width: 16px;
210 - text-align: center;
382 + display: none;
383 }
384
213 -/* Step type colors */
214 -.process-step[data-type="agent"] .step-icon { color: #64b5f6; }
215 -.process-step[data-type="tool"] .step-icon { color: #81c784; }
216 -.process-step[data-type="code_exe"] .step-icon { color: #ba68c8; }
217 -.process-step[data-type="browser"] .step-icon { color: #ffb74d; }
218 -.process-step[data-type="info"] .step-icon { color: #90a4ae; }
219 -.process-step[data-type="util"] .step-icon { color: #78909c; }
220 -.process-step[data-type="hint"] .step-icon { color: #aed581; }
221 -.process-step[data-type="warning"] .step-icon { color: #ffd54f; }
222 -.process-step[data-type="error"] .step-icon { color: #e57373; }
223 -
385 +/* Step type label removed - using status badges instead */
386 .process-step-header .step-type {
225 - font-size: 0.7rem;
226 - font-weight: 500;
227 - opacity: 0.5;
228 - min-width: 50px;
229 - text-transform: uppercase;
230 - letter-spacing: 0.3px;
387 + display: none;
388 }
389
390 +/* Step title */
391 .process-step-header .step-title {
392 flex: 1;
393 font-size: 0.75rem;
394 color: var(--color-text);
237 - opacity: 0.7;
395 + opacity: 0.85;
396 white-space: nowrap;
397 overflow: hidden;
398 text-overflow: ellipsis;
399 + margin-left: var(--spacing-xs);
400 }
401
243 -.process-step-header .step-time {
244 - font-size: 0.65rem;
245 - color: #81c784;
246 - flex-shrink: 0;
247 - font-family: var(--font-family-code);
248 - min-width: 35px;
249 - text-align: right;
250 -}
251 -
402 +/* Step expand icon - CSS triangle (no Material Icons) */
403 .process-step-header .step-expand-icon {
253 - font-size: 0.8rem;
404 + display: inline-block;
405 + width: 0;
406 + height: 0;
407 + border-style: solid;
408 + border-width: 3px 0 3px 5px;
409 + border-color: transparent transparent transparent var(--color-text);
410 opacity: 0.4;
411 transition: transform 0.2s ease, opacity 0.15s ease;
412 + flex-shrink: 0;
413 + font-size: 0; /* Hide any text content */
414 + margin-right: 2px;
415 }
416
417 .process-step-header:hover .step-expand-icon {
@@ -260,7 +419,7 @@
419 }
420
421 .process-step.step-expanded .step-expand-icon {
263 - transform: rotate(180deg);
422 + transform: rotate(90deg);
423 }
424
425 /* Step Detail Content - Animated expand/collapse */
@@ -283,16 +442,14 @@
442 }
443
444 .process-step-detail-content {
286 - padding: var(--spacing-xs) var(--spacing-sm);
445 + padding: var(--spacing-xs) 0;
446 margin-top: var(--spacing-xxs);
447 margin-left: 20px; /* Align with icon */
289 - background-color: rgba(0, 0, 0, 0.15);
290 - border-radius: 4px;
448 + background-color: transparent;
449 font-size: 0.7rem;
450 line-height: 1.5;
451 max-height: 300px;
452 overflow-y: auto;
295 - border-left: 2px solid rgba(255, 255, 255, 0.08);
453 }
454
455 .process-step-detail-content pre {
@@ -332,10 +489,126 @@
489 font-size: 0.7rem;
490 }
491
492 +/* Thoughts styling - single icon with plain text */
493 +.process-step-detail-content .step-thoughts {
494 + display: flex;
495 + align-items: flex-start;
496 + gap: 6px;
497 + margin: var(--spacing-xs) 0;
498 +}
499 +
500 +.process-step-detail-content .thought-icon {
501 + font-size: 0.85rem;
502 + color: #fbbf24;
503 + flex-shrink: 0;
504 + margin-top: 2px;
505 +}
506 +
507 +.process-step-detail-content .thought-text {
508 + font-size: 0.72rem;
509 + color: var(--color-text);
510 + opacity: 0.8;
511 + line-height: 1.5;
512 + white-space: pre-wrap;
513 + word-break: break-word;
514 +}
515 +
516 +/* Light mode thoughts */
517 +.light-mode .process-step-detail-content .thought-icon {
518 + color: #b45309;
519 +}
520 +
521 +/* Tool arguments structured display - clean label:value format without boxes */
522 +.process-step-detail-content .step-tool-args {
523 + display: flex;
524 + flex-direction: column;
525 + gap: 4px;
526 + margin: var(--spacing-xs) 0;
527 + padding: 0;
528 + background: transparent;
529 + border-radius: 0;
530 + border-left: none;
531 +}
532 +
533 +.process-step-detail-content .tool-arg-row {
534 + display: flex;
535 + gap: 8px;
536 + align-items: baseline;
537 +}
538 +
539 +.process-step-detail-content .tool-arg-label {
540 + font-size: 0.68rem;
541 + font-weight: 600;
542 + color: var(--color-primary);
543 + min-width: 70px;
544 + flex-shrink: 0;
545 +}
546 +
547 +.process-step-detail-content .tool-arg-value {
548 + font-size: 0.7rem;
549 + color: var(--color-text);
550 + opacity: 0.85;
551 + word-break: break-word;
552 + font-family: var(--font-family-code);
553 +}
554 +
555 +/* Light mode tool args - transparent like dark mode */
556 +.light-mode .process-step-detail-content .step-tool-args {
557 + background: transparent;
558 + border-left-color: transparent;
559 +}
560 +
561 +/* Terminal-style output for code_exe */
562 +.process-step-detail-content .step-terminal {
563 + margin: var(--spacing-xs) 0;
564 + font-family: var(--font-family-code);
565 + font-size: 0.72rem;
566 +}
567 +
568 +.process-step-detail-content .terminal-cmd {
569 + display: flex;
570 + align-items: baseline;
571 + gap: 4px;
572 + color: var(--color-text);
573 + opacity: 0.9;
574 +}
575 +
576 +.process-step-detail-content .terminal-prompt {
577 + color: #7ee787;
578 + font-weight: 600;
579 +}
580 +
581 +.process-step-detail-content .terminal-code {
582 + color: var(--color-text);
583 + opacity: 0.85;
584 +}
585 +
586 +.process-step-detail-content .terminal-output {
587 + margin: var(--spacing-xs) 0 0 0;
588 + padding: var(--spacing-xs);
589 + background: rgba(0, 0, 0, 0.2);
590 + border-radius: 4px;
591 + color: #c9d1d9;
592 + white-space: pre-wrap;
593 + word-break: break-word;
594 + max-height: 200px;
595 + overflow-y: auto;
596 +}
597 +
598 +/* Light mode terminal */
599 +.light-mode .process-step-detail-content .terminal-prompt {
600 + color: #1a7f37;
601 +}
602 +
603 +.light-mode .process-step-detail-content .terminal-output {
604 + background: rgba(0, 0, 0, 0.05);
605 + color: #24292f;
606 +}
607 +
608 /* Light mode adjustments */
609 .light-mode .process-group {
610 background: rgba(0, 0, 0, 0.03);
338 - border-color: rgba(0, 0, 0, 0.08);
611 + border: none;
612 }
613
614 .light-mode .process-group.embedded {
@@ -361,8 +634,7 @@
634 }
635
636 .light-mode .process-step-detail-content {
364 - background-color: rgba(0, 0, 0, 0.03);
365 - border-left-color: rgba(0, 0, 0, 0.08);
637 + background-color: transparent;
638 }
639
640 .light-mode .process-group-header .step-count {
@@ -371,22 +643,73 @@
643
644 /* Light mode text colors for process group */
645 .light-mode .process-group-header .group-title,
374 -.light-mode .process-group-header .step-count,
375 -.light-mode .process-step-header .step-title,
376 -.light-mode .process-step-header .step-type {
377 - color: #188216;
646 +.light-mode .process-step-header .step-title {
647 + color: var(--color-text);
648 +}
649 +
650 +.light-mode .process-group-header .group-metrics {
651 + color: var(--color-text);
652 +}
653 +
654 +/* Light mode status badge adjustments for better contrast */
655 +.light-mode .status-gen {
656 + background-color: rgba(14, 165, 233, 0.12);
657 + color: #0284c7;
658 + border-color: rgba(14, 165, 233, 0.25);
659 +}
660 +
661 +.light-mode .status-exe {
662 + background-color: rgba(147, 51, 234, 0.12);
663 + color: #7c3aed;
664 + border-color: rgba(147, 51, 234, 0.25);
665 +}
666 +
667 +.light-mode .status-mcp {
668 + background-color: rgba(217, 119, 6, 0.12);
669 + color: #b45309;
670 + border-color: rgba(217, 119, 6, 0.25);
671 +}
672 +
673 +.light-mode .status-mem {
674 + background-color: rgba(37, 99, 235, 0.12);
675 + color: #1d4ed8;
676 + border-color: rgba(37, 99, 235, 0.25);
677 +}
678 +
679 +.light-mode .status-end {
680 + background-color: rgba(22, 163, 74, 0.12);
681 + color: #15803d;
682 + border-color: rgba(22, 163, 74, 0.25);
683 +}
684 +
685 +.light-mode .status-err {
686 + background-color: rgba(220, 38, 38, 0.12);
687 + color: #b91c1c;
688 + border-color: rgba(220, 38, 38, 0.25);
689 +}
690 +
691 +.light-mode .status-wrn {
692 + background-color: rgba(234, 88, 12, 0.12);
693 + color: #c2410c;
694 + border-color: rgba(234, 88, 12, 0.25);
695 }
696
380 -.light-mode .process-group-header .group-timestamp {
381 - color: rgba(0, 0, 0, 0.5);
697 +.light-mode .status-sub {
698 + background-color: rgba(13, 148, 136, 0.12);
699 + color: #0f766e;
700 + border-color: rgba(13, 148, 136, 0.25);
701 }
702
384 -.light-mode .process-group-header .group-duration {
385 - color: #2e7d32;
703 +.light-mode .status-inf {
704 + background-color: rgba(100, 116, 139, 0.12);
705 + color: #475569;
706 + border-color: rgba(100, 116, 139, 0.25);
707 }
708
388 -.light-mode .process-step-header .step-time {
389 - color: #388e3c;
709 +.light-mode .status-utl {
710 + background-color: rgba(71, 85, 105, 0.12);
711 + color: #334155;
712 + border-color: rgba(71, 85, 105, 0.25);
713 }
714
715 /* Animation for loading state */
webui/css/messages.css
+120 -43
@@ -79,9 +79,12 @@
79
80 .message-user {
81 background-color: #4a4a4a;
82 + border-radius: var(--border-radius) !important;
83 /* border-bottom-right-radius: var(--spacing-xxs); */
84 /* min-width: 195px; */
85 text-align: end;
86 + box-shadow: inset 0 2rem 2rem -2rem rgba(0, 0, 0, 0.3),
87 + inset 0 -2rem 2rem -2rem rgba(0, 0, 0, 0.1);
88 }
89
90 .message-user > div {
@@ -116,7 +119,7 @@
119 }
120
121 .message-followup .message {
119 - border-radius: 1.125em; /* 18px */
122 + border-radius: 0;
123 /* border-top-left-radius: var(--spacing-xxs); */
124 }
125
@@ -137,31 +140,108 @@
140 .message-warning,
141 .message-error {
142 color: #e0e0e0;
143 + background-color: transparent;
144 + border-radius: 0;
145 + box-shadow: none;
146 }
147
148 .message-default {
143 - background-color: #1a242f;
149 + background-color: transparent;
150 }
151
152 .message-agent {
147 - background-color: #34506b;
153 + background-color: transparent;
154 }
155
156 .message-agent-response {
157 min-width: 255px;
152 - background-color: #1f3c1e;
158 + background-color: transparent;
159 }
160
161 .message-agent-delegation {
156 - background-color: #12685e;
162 + background-color: transparent;
163 }
164
165 .message-tool {
160 - background-color: #2a4170;
166 + background-color: transparent;
167 }
168
169 .message-code-exe {
164 - background-color: #4b3a69;
170 + background-color: transparent;
171 +}
172 +
173 +/* Terminal-style code execution block */
174 +.message-code-exe .message-body {
175 + background: #0d1117;
176 + border: 1px solid rgba(48, 54, 61, 0.8);
177 + border-radius: 6px;
178 + padding: 0;
179 + margin-top: 0.5em;
180 + overflow: hidden;
181 +}
182 +
183 +.message-code-exe .msg-heading {
184 + background: linear-gradient(180deg, #21262d 0%, #161b22 100%);
185 + padding: 6px 12px;
186 + border-bottom: 1px solid rgba(48, 54, 61, 0.8);
187 + display: flex;
188 + align-items: center;
189 + gap: 8px;
190 +}
191 +
192 +.message-code-exe .msg-heading h4 {
193 + font-family: var(--font-family-code);
194 + font-size: 0.75rem;
195 + color: #8b949e;
196 + margin: 0;
197 + display: flex;
198 + align-items: center;
199 + gap: 6px;
200 +}
201 +
202 +.message-code-exe .msg-heading h4::before {
203 + content: "$";
204 + color: #7ee787;
205 + font-weight: 600;
206 +}
207 +
208 +.message-code-exe .msg-content {
209 + padding: 12px;
210 + font-family: var(--font-family-code);
211 + font-size: 0.72rem;
212 + color: #c9d1d9;
213 + line-height: 1.5;
214 + max-height: 300px;
215 + overflow-y: auto;
216 +}
217 +
218 +.message-code-exe .msg-content pre {
219 + margin: 0;
220 + white-space: pre-wrap;
221 + word-break: break-word;
222 +}
223 +
224 +/* Light mode terminal */
225 +.light-mode .message-code-exe .message-body {
226 + background: #f6f8fa;
227 + border-color: #d0d7de;
228 +}
229 +
230 +.light-mode .message-code-exe .msg-heading {
231 + background: linear-gradient(180deg, #f6f8fa 0%, #eaeef2 100%);
232 + border-bottom-color: #d0d7de;
233 +}
234 +
235 +.light-mode .message-code-exe .msg-heading h4 {
236 + color: #57606a;
237 +}
238 +
239 +.light-mode .message-code-exe .msg-heading h4::before {
240 + color: #1a7f37;
241 +}
242 +
243 +.light-mode .message-code-exe .msg-content {
244 + color: #24292f;
245 }
246
247 .message-body .message-markdown-table-wrap {
@@ -181,40 +261,33 @@
261 white-space: break-spaces;
262 }
263
184 -.light-mode .message-code-exe .message-body {
185 - border: 1px solid var(--color-border);
186 -}
264 +/* Light mode code-exe styling moved to main terminal block above */
265
266 .message-browser {
189 - background-color: #4b3a69;
267 + background-color: transparent;
268 }
269
270 .message-info {
193 - background-color: var(--color-panel);
271 + background-color: transparent;
272 }
273
274 .message-util {
197 - background-color: #23211a;
275 + background-color: transparent;
276 display: none;
277 }
278
279 .message-warning {
202 - background-color: #bc8036;
280 + background-color: transparent;
281 }
282
283 .message-error {
206 - background-color: #af2222;
284 + background-color: rgba(180, 40, 40, 0.25);
285 + border: 1px solid rgba(220, 60, 60, 0.5);
286 + border-radius: 8px;
287 + padding: 12px;
288 }
289
209 -.message-code-exe .message-body {
210 - min-height: 5em;
211 - width: 100%;
212 - background-color: var(--color-panel);
213 - border-radius: 0.5em;
214 - margin-top: 0.5em;
215 - padding: 0.3em;
216 - font-family: var(--font-family-code);
217 -}
290 +/* Terminal styling moved to new terminal block above */
291
292 /* Agent and AI Info */
293 .agent-start {
@@ -254,7 +327,7 @@
327 }
328
329 .msg-kvps tr {
257 - border-bottom: 1px solid rgba(255, 255, 255, 0.15);
330 + border-bottom: none;
331 }
332
333 .msg-heading {
@@ -415,67 +488,71 @@
488 }
489
490 .light-mode .msg-kvps tr {
418 - border-bottom: 1px solid rgb(192 192 192 / 50%);
491 + border-bottom: none;
492 }
493
494 .light-mode .message-default {
422 - background-color: var(--color-panel);
495 + background-color: transparent;
496 color: #1a242f;
497 }
498
499 .light-mode .message-agent {
427 - background-color: var(--color-panel);
500 + background-color: transparent;
501 color: #356ca3;
502 }
503
504 .light-mode .message-agent-response {
432 - background-color: var(--color-panel);
505 + background-color: transparent;
506 color: #188216;
507 }
508
509 .light-mode .message-agent-delegation {
437 - background-color: var(--color-panel);
510 + background-color: transparent;
511 color: #12685e;
512 }
513
514 .light-mode .message-tool {
442 - background-color: var(--color-panel);
515 + background-color: transparent;
516 color: #1c3c88;
517 }
518
519 .light-mode .message-code-exe {
447 - background-color: var(--color-panel);
520 + background-color: transparent;
521 color: #6c43b0;
522 }
523
524 .light-mode .message-browser {
452 - background-color: var(--color-panel);
525 + background-color: transparent;
526 color: #6c43b0;
527 }
528
529 .light-mode .message-info {
457 - background-color: var(--color-panel);
530 + background-color: transparent;
531 color: #3f3f3f;
532 }
533
534 .light-mode .message-util {
462 - background-color: var(--color-panel);
535 + background-color: transparent;
536 color: #5b5540;
537 }
538
539 .light-mode .message-warning {
467 - background-color: var(--color-panel);
540 + background-color: transparent;
541 color: #8f4800;
542 }
543
544 .light-mode .message-error {
472 - background-color: var(--color-panel);
545 + background-color: rgba(220, 60, 60, 0.15);
546 + border: 1px solid rgba(180, 40, 40, 0.4);
547 color: #8f1010;
548 }
549
550 .light-mode .message-user {
551 background-color: var(--color-panel);
552 color: #4e4e4e;
553 + border-radius: var(--border-radius);
554 + box-shadow: inset 0 2rem 2rem -2rem rgba(0, 0, 0, 0.15),
555 + inset 0 -2rem 2rem -2rem rgba(0, 0, 0, 0.05);
556 }
557
558 /* Markdown in messages */
@@ -604,18 +681,18 @@
681 }
682
683 /* 1. FIRST child’s .message – clear ONLY bottom corners */
607 -.message-group > *:first-child:not(:last-child) > .message {
684 +.message-group > *:first-child:not(:last-child) > .message-user {
685 border-bottom-left-radius: var(--spacing-xxs);
686 border-bottom-right-radius: var(--spacing-xxs);
687 }
688
689 /* 2. MIDDLE children’s .message – clear ALL corners */
613 -.message-group > *:not(:first-child):not(:last-child) > .message {
690 +.message-group > *:not(:first-child):not(:last-child) > .message-user {
691 border-radius: var(--spacing-xxs);
692 }
693
694 /* 3. LAST child’s .message – clear ONLY top corners */
618 -.message-group > *:last-child:not(:first-child) > .message {
695 +.message-group > *:last-child:not(:first-child) > .message-user {
696 border-top-left-radius: var(--spacing-xxs);
697 border-top-right-radius: var(--spacing-xxs);
698 }
@@ -630,7 +707,7 @@
707
708 .message {
709 /* background-color: var(--color-message-bg); */
633 - border-radius: var(--border-radius);
710 + border-radius: 0;
711 padding: 0.9rem var(--spacing-sm) 0.7rem var(--spacing-sm);
712 overflow-x: auto;
713 width: auto;
@@ -639,10 +716,10 @@
716 /* display: block; */
717 word-break: break-word;
718 overflow-wrap: anywhere;
719 + box-shadow: none;
720 }
721
722 /* shades */
723 .dark-mode .message {
646 - box-shadow: inset 0 2rem 2rem -2rem rgba(0, 0, 0, 0.3),
647 - inset 0 -2rem 2rem -2rem rgba(0, 0, 0, 0.1);
724 + box-shadow: none;
725 }
webui/index.css
+9 -4
@@ -196,14 +196,19 @@ div#right-panel::-webkit-scrollbar-thumb:hover {
196
197 #time-date {
198 color: var(--color-text);
199 - font-size: var(--font-size-normal);
199 + font-size: 1.5rem;
200 + font-weight: 500;
201 text-align: right;
201 - line-height: 1.1;
202 + line-height: 1.2;
203 + font-family: var(--font-family-code);
204 }
205
206 #user-date {
205 - font-size: var(--font-size-small);
206 - opacity: 0.6;
207 + font-size: 0.7rem;
208 + opacity: 0.5;
209 + font-weight: 400;
210 + display: block;
211 + margin-top: 2px;
212 }
213
214 /* Typography */
webui/index.js
+6 -3
@@ -185,8 +185,8 @@ async function updateUserTime() {
185 updateUserTime();
186 setInterval(updateUserTime, 1000);
187
188 -function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null) {
189 - const result = msgs.setMessage(id, type, heading, content, temp, kvps, timestamp);
188 +function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, tokensIn = 0, tokensOut = 0) {
189 + const result = msgs.setMessage(id, type, heading, content, temp, kvps, timestamp, durationMs, tokensIn, tokensOut);
190 const chatHistoryEl = document.getElementById("chat-history");
191 if (preferencesStore.autoScroll && chatHistoryEl) {
192 chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight;
@@ -311,7 +311,10 @@ export async function poll() {
311 log.content,
312 log.temp,
313 log.kvps,
314 - log.timestamp
314 + log.timestamp,
315 + log.duration_ms,
316 + log.tokens_in,
317 + log.tokens_out
318 );
319 }
320 afterMessagesUpdate(response.logs);
webui/js/messages.js
+324 -77
@@ -13,9 +13,9 @@ let messageGroup = null;
13 let currentProcessGroup = null; // Track current process group for collapsible UI
14
15 // Process types that should be grouped into collapsible sections
16 -const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'info', 'hint', 'util'];
16 +const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'info', 'hint', 'util', 'warning'];
17 // Main types that should always be visible (not collapsed)
18 -const MAIN_TYPES = ['user', 'response', 'warning', 'error', 'rate_limit'];
18 +const MAIN_TYPES = ['user', 'response', 'error', 'rate_limit'];
19
20 /**
21 * Check if a response is from the main agent (A0)
@@ -29,7 +29,7 @@ function isMainAgentResponse(heading) {
29 return match[1] === "0"; // Only A0 is the main agent
30 }
31
32 -export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null) {
32 +export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, tokensIn = 0, tokensOut = 0) {
33 // Check if this is a process type message
34 const isProcessType = PROCESS_TYPES.includes(type);
35 const isMainType = MAIN_TYPES.includes(type);
@@ -48,7 +48,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
48 if (isProcessType) {
49 if (processStepElement) {
50 // Update existing process step
51 - updateProcessStep(processStepElement, id, type, heading, content, kvps);
51 + updateProcessStep(processStepElement, id, type, heading, content, kvps, durationMs, tokensIn, tokensOut);
52 return processStepElement;
53 }
54
@@ -59,14 +59,14 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
59 }
60
61 // Add step to current process group
62 - processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp);
62 + processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp, durationMs, tokensIn, tokensOut);
63 return processStepElement;
64 }
65
66 // For subordinate agent responses (A1, A2, ...), treat as a process step instead of main response
67 if (type === "response" && !isMainAgentResponse(heading)) {
68 if (processStepElement) {
69 - updateProcessStep(processStepElement, id, "agent", heading, content, kvps);
69 + updateProcessStep(processStepElement, id, "agent", heading, content, kvps, durationMs, tokensIn, tokensOut);
70 return processStepElement;
71 }
72
@@ -77,15 +77,18 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
77 }
78
79 // Add subordinate response as a step (type "agent" for appropriate styling)
80 - processStepElement = addProcessStep(currentProcessGroup, id, "agent", heading, content, kvps, timestamp);
80 + processStepElement = addProcessStep(currentProcessGroup, id, "agent", heading, content, kvps, timestamp, durationMs, tokensIn, tokensOut);
81 return processStepElement;
82 }
83
84 - // For main agent (A0) response, embed the current process group
84 + // For main agent (A0) response, embed the current process group and mark as complete
85 if (type === "response" && currentProcessGroup) {
86 const processGroupToEmbed = currentProcessGroup;
87 // Keep currentProcessGroup reference - subsequent process messages go to same group
88
89 + // Mark process group as complete (END state)
90 + markProcessGroupComplete(processGroupToEmbed, heading);
91 +
92 if (!messageContainer) {
93 // Create new container with embedded process group
94 messageContainer = createResponseContainerWithProcessGroup(id, processGroupToEmbed);
@@ -494,14 +497,11 @@ export function drawMessageUser(
497 messageDiv.className = "message message-user";
498 }
499
497 - // Handle heading
500 + // Remove heading element if it exists (user messages no longer show label per target design)
501 let headingElement = messageDiv.querySelector(".msg-heading");
499 - if (!headingElement) {
500 - headingElement = document.createElement("h4");
501 - headingElement.classList.add("msg-heading");
502 - messageDiv.insertBefore(headingElement, messageDiv.firstChild);
502 + if (headingElement) {
503 + headingElement.remove();
504 }
504 - headingElement.innerHTML = `${heading} <span class='icon material-symbols-outlined'>person</span>`;
505
506 // Handle content
507 let textDiv = messageDiv.querySelector(".message-text");
@@ -1161,11 +1161,15 @@ function createProcessGroup(id) {
1161 const header = document.createElement("div");
1162 header.classList.add("process-group-header");
1163 header.innerHTML = `
1164 - <span class="material-symbols-outlined expand-icon">chevron_right</span>
1165 - <span class="material-symbols-outlined group-icon">neurology</span>
1164 + <span class="expand-icon"></span>
1165 + <span class="status-badge status-gen status-active group-status"><span class="badge-icon material-symbols-outlined">public</span>GEN</span>
1166 <span class="group-title">Processing...</span>
1167 - <span class="step-count">0 steps</span>
1168 - <span class="group-timestamp"></span>
1167 + <span class="group-metrics">
1168 + <span class="metric-time" title="Start time"><span class="metric-value">--:--</span></span>
1169 + <span class="metric-steps" title="Steps"><span class="metric-value">0</span></span>
1170 + <span class="metric-duration" title="Duration"><span class="metric-value">0s</span></span>
1171 + <span class="metric-tokens" title="Tokens"><span class="metric-value">--</span></span>
1172 + </span>
1173 `;
1174
1175 // Add click handler for expansion
@@ -1194,7 +1198,7 @@ function createProcessGroup(id) {
1198 /**
1199 * Add a step to a process group
1200 */
1197 -function addProcessStep(group, id, type, heading, content, kvps, timestamp = null) {
1201 +function addProcessStep(group, id, type, heading, content, kvps, timestamp = null, durationMs = null, tokensIn = 0, tokensOut = 0) {
1202 const groupId = group.getAttribute("data-group-id");
1203 const stepsContainer = group.querySelector(".process-steps");
1204
@@ -1204,6 +1208,8 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1208 step.classList.add("process-step");
1209 step.setAttribute("data-type", type);
1210 step.setAttribute("data-step-id", id);
1211 + step.setAttribute("data-tokens-in", tokensIn || 0);
1212 + step.setAttribute("data-tokens-out", tokensOut || 0);
1213
1214 // Store timestamp for duration calculation
1215 if (timestamp) {
@@ -1244,22 +1250,17 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1250 const stepHeader = document.createElement("div");
1251 stepHeader.classList.add("process-step-header");
1252
1247 - // Calculate relative time from group start
1248 - let relativeTimeStr = "";
1249 - if (timestamp) {
1250 - const groupStartTime = parseFloat(group.getAttribute("data-start-timestamp") || "0");
1251 - if (groupStartTime > 0) {
1252 - const relativeMs = (timestamp - groupStartTime) * 1000;
1253 - relativeTimeStr = formatRelativeTime(relativeMs);
1254 - }
1255 - }
1253 + // Get 3-letter status code and color class
1254 + const statusCode = processGroupStore.getStepCode(type);
1255 + const statusColorClass = processGroupStore.getStatusColorClass(type);
1256 +
1257 + // Get icon for step type
1258 + const stepIcon = processGroupStore.getStepBadgeIcon(type);
1259
1260 stepHeader.innerHTML = `
1258 - <span class="material-symbols-outlined step-icon">${icon}</span>
1259 - <span class="step-type">${label}</span>
1261 + <span class="step-expand-icon"></span>
1262 + <span class="status-badge ${statusColorClass} status-active"><span class="badge-icon material-symbols-outlined">${stepIcon}</span>${statusCode}</span>
1263 <span class="step-title">${escapeHTML(title)}</span>
1261 - ${relativeTimeStr ? `<span class="step-time">${relativeTimeStr}</span>` : ""}
1262 - <span class="material-symbols-outlined step-expand-icon">expand_more</span>
1264 `;
1265
1266 // Add click handler for step expansion
@@ -1279,11 +1280,15 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1280 detailContent.classList.add("process-step-detail-content");
1281
1282 // Add content to detail
1282 - renderStepDetailContent(detailContent, content, kvps);
1283 + renderStepDetailContent(detailContent, content, kvps, type);
1284
1285 detail.appendChild(detailContent);
1286 step.appendChild(detail);
1287
1288 + // Remove status-active from all previous steps (only the current step is active)
1289 + const prevSteps = stepsContainer.querySelectorAll(".process-step .status-badge.status-active");
1290 + prevSteps.forEach(badge => badge.classList.remove("status-active"));
1291 +
1292 stepsContainer.appendChild(step);
1293
1294 // Update group header
@@ -1295,7 +1300,7 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1300 /**
1301 * Update an existing process step
1302 */
1298 -function updateProcessStep(stepElement, id, type, heading, content, kvps) {
1303 +function updateProcessStep(stepElement, id, type, heading, content, kvps, durationMs = null, tokensIn = 0, tokensOut = 0) {
1304 // Update title
1305 const titleEl = stepElement.querySelector(".step-title");
1306 if (titleEl) {
@@ -1303,10 +1308,18 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps) {
1308 titleEl.textContent = title;
1309 }
1310
1311 + // Update token data (use the latest values)
1312 + if (tokensIn > 0) {
1313 + stepElement.setAttribute("data-tokens-in", tokensIn);
1314 + }
1315 + if (tokensOut > 0) {
1316 + stepElement.setAttribute("data-tokens-out", tokensOut);
1317 + }
1318 +
1319 // Update detail content
1320 const detailContent = stepElement.querySelector(".process-step-detail-content");
1321 if (detailContent) {
1309 - renderStepDetailContent(detailContent, content, kvps);
1322 + renderStepDetailContent(detailContent, content, kvps, type);
1323 }
1324
1325 // Update parent group header
@@ -1369,9 +1382,48 @@ function cleanStepTitle(text, maxLength) {
1382 /**
1383 * Render content for step detail panel
1384 */
1372 -function renderStepDetailContent(container, content, kvps) {
1385 +function renderStepDetailContent(container, content, kvps, type = null) {
1386 container.innerHTML = "";
1387
1388 + // Special handling for code_exe type - render as terminal-style output
1389 + if (type === "code_exe" && kvps) {
1390 + const runtime = kvps.runtime || kvps.Runtime || "bash";
1391 + const code = kvps.code || kvps.Code || "";
1392 + const output = content || "";
1393 +
1394 + if (code || output) {
1395 + const terminalDiv = document.createElement("div");
1396 + terminalDiv.classList.add("step-terminal");
1397 +
1398 + // Show command line
1399 + if (code) {
1400 + const cmdLine = document.createElement("div");
1401 + cmdLine.classList.add("terminal-cmd");
1402 + cmdLine.innerHTML = `<span class="terminal-prompt">${escapeHTML(runtime)}></span> <span class="terminal-code">${escapeHTML(code)}</span>`;
1403 + terminalDiv.appendChild(cmdLine);
1404 + }
1405 +
1406 + // Show output if present
1407 + if (output && output.trim()) {
1408 + const outputPre = document.createElement("pre");
1409 + outputPre.classList.add("terminal-output");
1410 + outputPre.textContent = truncateText(output, 1000);
1411 + terminalDiv.appendChild(outputPre);
1412 + }
1413 +
1414 + container.appendChild(terminalDiv);
1415 + }
1416 +
1417 + // Still render thoughts if present
1418 + if (kvps.thoughts || kvps.thinking || kvps.reasoning) {
1419 + const thoughtKey = kvps.thoughts ? "thoughts" : (kvps.thinking ? "thinking" : "reasoning");
1420 + const thoughtValue = kvps[thoughtKey];
1421 + renderThoughts(container, thoughtValue);
1422 + }
1423 +
1424 + return;
1425 + }
1426 +
1427 // Add KVPs if present
1428 if (kvps && Object.keys(kvps).length > 0) {
1429 const kvpsDiv = document.createElement("div");
@@ -1381,20 +1433,79 @@ function renderStepDetailContent(container, content, kvps) {
1433 // Skip internal/display keys
1434 if (key === "finished" || key === "attachments") continue;
1435
1384 - const kvpDiv = document.createElement("div");
1385 - kvpDiv.classList.add("step-kvp");
1436 + // Skip code_exe specific keys that we handle specially above
1437 + if (type === "code_exe" && (key.toLowerCase() === "runtime" || key.toLowerCase() === "session" || key.toLowerCase() === "code")) {
1438 + continue;
1439 + }
1440
1387 - // Add msg-thoughts class for thoughts-related keys (controlled by showThoughts preference)
1441 const lowerKey = key.toLowerCase();
1442
1390 - if (lowerKey === "thoughts" || lowerKey === "thinking" || lowerKey === "reflection") {
1391 - kvpDiv.classList.add("msg-thoughts");
1443 + // Special handling for thoughts/reasoning - render with single lightbulb icon
1444 + if (lowerKey === "thoughts" || lowerKey === "thinking" || lowerKey === "reflection" || lowerKey === "reasoning") {
1445 + const thoughtsDiv = document.createElement("div");
1446 + thoughtsDiv.classList.add("step-thoughts", "msg-thoughts");
1447 +
1448 // Apply current preference state - hide if showThoughts is false
1449 if (!preferencesStore.showThoughts) {
1394 - kvpDiv.classList.add("hide-thoughts");
1450 + thoughtsDiv.classList.add("hide-thoughts");
1451 + }
1452 +
1453 + let thoughtText = value;
1454 + if (typeof value === "object") {
1455 + // Handle array of thoughts
1456 + if (Array.isArray(value)) {
1457 + thoughtText = value.filter(t => t && String(t).trim() && String(t).trim() !== "[" && String(t).trim() !== "]").join("\n");
1458 + } else {
1459 + thoughtText = JSON.stringify(value, null, 2);
1460 + }
1461 + }
1462 +
1463 + // Clean up the text - remove standalone brackets
1464 + thoughtText = String(thoughtText).replace(/^\s*[\[\]]\s*$/gm, "").trim();
1465 +
1466 + if (thoughtText) {
1467 + // Single icon + text block
1468 + thoughtsDiv.innerHTML = `<span class="thought-icon material-symbols-outlined">lightbulb</span><span class="thought-text">${escapeHTML(thoughtText)}</span>`;
1469 + }
1470 +
1471 + kvpsDiv.appendChild(thoughtsDiv);
1472 + continue;
1473 + }
1474 +
1475 + // Special handling for tool_args - render as structured labeled parameters
1476 + if (lowerKey === "tool_args" && typeof value === "object" && value !== null) {
1477 + const argsDiv = document.createElement("div");
1478 + argsDiv.classList.add("step-tool-args");
1479 +
1480 + for (const [argKey, argValue] of Object.entries(value)) {
1481 + const argRow = document.createElement("div");
1482 + argRow.classList.add("tool-arg-row");
1483 +
1484 + const argLabel = document.createElement("span");
1485 + argLabel.classList.add("tool-arg-label");
1486 + argLabel.textContent = convertToTitleCase(argKey) + ":";
1487 +
1488 + const argVal = document.createElement("span");
1489 + argVal.classList.add("tool-arg-value");
1490 +
1491 + let argText = argValue;
1492 + if (typeof argValue === "object") {
1493 + argText = JSON.stringify(argValue, null, 2);
1494 + }
1495 + argVal.textContent = truncateText(String(argText), 300);
1496 +
1497 + argRow.appendChild(argLabel);
1498 + argRow.appendChild(argVal);
1499 + argsDiv.appendChild(argRow);
1500 }
1501 +
1502 + kvpsDiv.appendChild(argsDiv);
1503 + continue;
1504 }
1505
1506 + const kvpDiv = document.createElement("div");
1507 + kvpDiv.classList.add("step-kvp");
1508 +
1509 const keySpan = document.createElement("span");
1510 keySpan.classList.add("step-kvp-key");
1511 keySpan.textContent = convertToTitleCase(key) + ":";
@@ -1431,39 +1542,124 @@ function renderStepDetailContent(container, content, kvps) {
1542 }
1543
1544 /**
1434 - * Update process group header with step count and status
1545 + * Helper to render thoughts/reasoning with lightbulb icon
1546 + */
1547 +function renderThoughts(container, value) {
1548 + const thoughtsDiv = document.createElement("div");
1549 + thoughtsDiv.classList.add("step-thoughts", "msg-thoughts");
1550 +
1551 + // Apply current preference state - hide if showThoughts is false
1552 + if (!preferencesStore.showThoughts) {
1553 + thoughtsDiv.classList.add("hide-thoughts");
1554 + }
1555 +
1556 + let thoughtText = value;
1557 + if (typeof value === "object") {
1558 + if (Array.isArray(value)) {
1559 + thoughtText = value.filter(t => t && String(t).trim() && String(t).trim() !== "[" && String(t).trim() !== "]").join("\n");
1560 + } else {
1561 + thoughtText = JSON.stringify(value, null, 2);
1562 + }
1563 + }
1564 +
1565 + thoughtText = String(thoughtText).replace(/^\s*[\[\]]\s*$/gm, "").trim();
1566 +
1567 + if (thoughtText) {
1568 + thoughtsDiv.innerHTML = `<span class="thought-icon material-symbols-outlined">lightbulb</span><span class="thought-text">${escapeHTML(thoughtText)}</span>`;
1569 + container.appendChild(thoughtsDiv);
1570 + }
1571 +}
1572 +
1573 +/**
1574 + * Update process group header with step count, status, and metrics
1575 */
1576 function updateProcessGroupHeader(group) {
1577 const steps = group.querySelectorAll(".process-step");
1438 - const countEl = group.querySelector(".step-count");
1578 const titleEl = group.querySelector(".group-title");
1579 + const statusEl = group.querySelector(".group-status");
1580 + const metricsEl = group.querySelector(".group-metrics");
1581 +
1582 + // Update step count in metrics
1583 + const stepsMetricEl = metricsEl?.querySelector(".metric-steps .metric-value");
1584 + if (stepsMetricEl) {
1585 + stepsMetricEl.textContent = steps.length.toString();
1586 + }
1587 +
1588 + // Update time metric
1589 + const timeMetricEl = metricsEl?.querySelector(".metric-time .metric-value");
1590 + const startTimestamp = group.getAttribute("data-start-timestamp");
1591 + if (timeMetricEl && startTimestamp) {
1592 + const date = new Date(parseFloat(startTimestamp) * 1000);
1593 + const hours = String(date.getHours()).padStart(2, "0");
1594 + const minutes = String(date.getMinutes()).padStart(2, "0");
1595 + timeMetricEl.textContent = `${hours}:${minutes}`;
1596 + }
1597
1441 - if (countEl) {
1442 - const count = steps.length;
1443 - countEl.textContent = `${count} step${count !== 1 ? "s" : ""}`;
1598 + // Update duration metric (elapsed time since start)
1599 + const durationMetricEl = metricsEl?.querySelector(".metric-duration .metric-value");
1600 + if (durationMetricEl && startTimestamp) {
1601 + const startMs = parseFloat(startTimestamp) * 1000;
1602 + const elapsedMs = Date.now() - startMs;
1603 + if (elapsedMs < 60000) {
1604 + durationMetricEl.textContent = `${Math.round(elapsedMs / 1000)}s`;
1605 + } else {
1606 + const mins = Math.floor(elapsedMs / 60000);
1607 + const secs = Math.round((elapsedMs % 60000) / 1000);
1608 + durationMetricEl.textContent = `${mins}m${secs}s`;
1609 + }
1610 }
1611
1446 - if (titleEl && steps.length > 0) {
1447 - // Get the last step's type for the title
1612 + // Update tokens metric (aggregate from all steps)
1613 + const tokensMetricEl = metricsEl?.querySelector(".metric-tokens .metric-value");
1614 + if (tokensMetricEl) {
1615 + let totalTokensIn = 0;
1616 + let totalTokensOut = 0;
1617 + steps.forEach(step => {
1618 + totalTokensIn += parseInt(step.getAttribute("data-tokens-in") || 0, 10);
1619 + totalTokensOut += parseInt(step.getAttribute("data-tokens-out") || 0, 10);
1620 + });
1621 + const totalTokens = totalTokensIn + totalTokensOut;
1622 + if (totalTokens > 0) {
1623 + // Format as compact notation (e.g., 20k/3k for input/output)
1624 + tokensMetricEl.textContent = formatTokenCount(totalTokensIn, totalTokensOut);
1625 + } else {
1626 + tokensMetricEl.textContent = "--";
1627 + }
1628 + }
1629 +
1630 + if (steps.length > 0) {
1631 + // Get the last step's type for status
1632 const lastStep = steps[steps.length - 1];
1633 const lastType = lastStep.getAttribute("data-type");
1634 const lastTitle = lastStep.querySelector(".step-title")?.textContent || "";
1635
1452 - // Prefer agent type steps for the group title as they contain thinking/planning info
1453 - if (lastType === "agent" && lastTitle) {
1454 - titleEl.textContent = cleanStepTitle(lastTitle, 50);
1455 - } else {
1456 - // Try to find the most recent agent step for a better title
1457 - const agentSteps = group.querySelectorAll('.process-step[data-type="agent"]');
1458 - if (agentSteps.length > 0) {
1459 - const lastAgentStep = agentSteps[agentSteps.length - 1];
1460 - const agentTitle = lastAgentStep.querySelector(".step-title")?.textContent || "";
1461 - if (agentTitle) {
1462 - titleEl.textContent = cleanStepTitle(agentTitle, 50);
1463 - return;
1636 + // Update status badge with icon (keep status-active during execution)
1637 + if (statusEl) {
1638 + const statusCode = processGroupStore.getStepCode(lastType);
1639 + const statusColorClass = processGroupStore.getStatusColorClass(lastType);
1640 + const badgeIcon = processGroupStore.getStepBadgeIcon(lastType);
1641 + statusEl.innerHTML = `<span class="badge-icon material-symbols-outlined">${badgeIcon}</span>${statusCode}`;
1642 + statusEl.className = `status-badge ${statusColorClass} status-active group-status`;
1643 + }
1644 +
1645 + // Update title
1646 + if (titleEl) {
1647 + // Prefer agent type steps for the group title as they contain thinking/planning info
1648 + if (lastType === "agent" && lastTitle) {
1649 + titleEl.textContent = cleanStepTitle(lastTitle, 50);
1650 + } else {
1651 + // Try to find the most recent agent step for a better title
1652 + const agentSteps = group.querySelectorAll('.process-step[data-type="agent"]');
1653 + if (agentSteps.length > 0) {
1654 + const lastAgentStep = agentSteps[agentSteps.length - 1];
1655 + const agentTitle = lastAgentStep.querySelector(".step-title")?.textContent || "";
1656 + if (agentTitle) {
1657 + titleEl.textContent = cleanStepTitle(agentTitle, 50);
1658 + return;
1659 + }
1660 }
1661 + titleEl.textContent = cleanStepTitle(lastTitle, 50) || `Processing...`;
1662 }
1466 - titleEl.textContent = `Processing (${processGroupStore.getStepLabel(lastType)})`;
1663 }
1664 }
1665 }
@@ -1478,6 +1674,52 @@ function truncateText(text, maxLength) {
1674 return text.substring(0, maxLength - 3) + "...";
1675 }
1676
1677 +/**
1678 + * Mark a process group as complete (END state)
1679 + */
1680 +function markProcessGroupComplete(group, responseTitle) {
1681 + if (!group) return;
1682 +
1683 + // Update status badge to END (remove status-active)
1684 + const statusEl = group.querySelector(".group-status");
1685 + if (statusEl) {
1686 + statusEl.textContent = "END";
1687 + statusEl.className = "status-badge status-end group-status"; // No status-active
1688 + }
1689 +
1690 + // Remove status-active from all step badges (stop spinners)
1691 + const stepBadges = group.querySelectorAll(".process-step .status-badge.status-active");
1692 + stepBadges.forEach(badge => badge.classList.remove("status-active"));
1693 +
1694 + // Update title if response title is available
1695 + const titleEl = group.querySelector(".group-title");
1696 + if (titleEl && responseTitle) {
1697 + const cleanTitle = cleanStepTitle(responseTitle, 50);
1698 + if (cleanTitle) {
1699 + titleEl.textContent = cleanTitle;
1700 + }
1701 + }
1702 +
1703 + // Add completed class to group
1704 + group.classList.add("process-group-completed");
1705 +
1706 + // Update duration to final value
1707 + const metricsEl = group.querySelector(".group-metrics");
1708 + const durationMetricEl = metricsEl?.querySelector(".metric-duration .metric-value");
1709 + const startTimestamp = group.getAttribute("data-start-timestamp");
1710 + if (durationMetricEl && startTimestamp) {
1711 + const startMs = parseFloat(startTimestamp) * 1000;
1712 + const elapsedMs = Date.now() - startMs;
1713 + if (elapsedMs < 60000) {
1714 + durationMetricEl.textContent = `${Math.round(elapsedMs / 1000)}s`;
1715 + } else {
1716 + const mins = Math.floor(elapsedMs / 60000);
1717 + const secs = Math.round((elapsedMs % 60000) / 1000);
1718 + durationMetricEl.textContent = `${mins}m${secs}s`;
1719 + }
1720 + }
1721 +}
1722 +
1723 /**
1724 * Reset process group state (called on context switch)
1725 */
@@ -1486,6 +1728,26 @@ export function resetProcessGroups() {
1728 messageGroup = null;
1729 }
1730
1731 +/**
1732 + * Format token counts in compact notation (e.g., "12k/3k" for input/output)
1733 + */
1734 +function formatTokenCount(tokensIn, tokensOut) {
1735 + const formatCompact = (n) => {
1736 + if (n >= 1000000) return `${(n / 1000000).toFixed(1)}m`;
1737 + if (n >= 1000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k`;
1738 + return n.toString();
1739 + };
1740 +
1741 + if (tokensIn > 0 && tokensOut > 0) {
1742 + return `${formatCompact(tokensIn)}/${formatCompact(tokensOut)}`;
1743 + } else if (tokensIn > 0) {
1744 + return `${formatCompact(tokensIn)}↓`;
1745 + } else if (tokensOut > 0) {
1746 + return `${formatCompact(tokensOut)}↑`;
1747 + }
1748 + return "--";
1749 +}
1750 +
1751 /**
1752 * Format Unix timestamp as date-time string (YYYY-MM-DD HH:MM:SS)
1753 */
@@ -1500,18 +1762,3 @@ function formatDateTime(timestamp) {
1762 return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
1763 }
1764
1503 -/**
1504 - * Format relative time for steps (e.g., +0.5s, +2.3s)
1505 - */
1506 -function formatRelativeTime(ms) {
1507 - if (ms < 100) {
1508 - return "+0s";
1509 - }
1510 - const seconds = ms / 1000;
1511 - if (seconds < 60) {
1512 - return `+${seconds.toFixed(1)}s`;
1513 - }
1514 - const minutes = Math.floor(seconds / 60);
1515 - const remainingSeconds = Math.round(seconds % 60);
1516 - return `+${minutes}m${remainingSeconds}s`;
1517 -}