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
-}