chat rendering refactor - work in progress
frdel committed
Jan 25, 2026 at 19:12 UTC
f63df9f4ebbb506a2e9568915ae2edd98b9a8f0a
14 files changed
+1592
-1385
agent.py
+2
-2
@@ -831,8 +831,8 @@ class Agent:
831
tool_request = extract_tools.json_parse_dirty(msg)
832
833
if tool_request is not None:
834
- raw_tool_name = tool_request.get("tool_name", "") # Get the raw tool name
835
- tool_args = tool_request.get("tool_args", {})
834
+ raw_tool_name = tool_request.get("tool_name", tool_request.get("tool","")) # Get the raw tool name
835
+ tool_args = tool_request.get("tool_args", tool_request.get("args", {}))
836
837
tool_name = raw_tool_name # Initialize tool_name with raw_tool_name
838
tool_method = None # Initialize tool_method
models.py
+2
-2
@@ -115,8 +115,8 @@ class ChatGenerationResult:
115
# if the model outputs thinking tags, we ned to parse them manually as reasoning
116
processed_chunk = self._process_thinking_chunk(chunk)
117
118
- self.reasoning += processed_chunk["reasoning_delta"]
119
- self.response += processed_chunk["response_delta"]
118
+ self.reasoning += processed_chunk.get("reasoning_delta", "")
119
+ self.response += processed_chunk.get("response_delta", "")
120
121
return processed_chunk
122
python/extensions/before_main_llm_call/_10_log_for_stream.py
+1
-1
@@ -22,7 +22,7 @@ class LogForStream(Extension):
22
def build_heading(agent, text: str, icon: str = "network_intelligence"):
23
# Include agent identifier for all agents (A0:, A1:, A2:, etc.)
24
agent_prefix = f"{agent.agent_name}: "
25
- return f"icon://{icon} {agent_prefix}{text}"
25
+ return f"{agent_prefix}{text}"
26
27
def build_default_heading(agent):
28
return build_heading(agent, "Generating...")
\ No newline at end of file
python/extensions/response_stream/_10_log_from_stream.py
+14
@@ -42,7 +42,21 @@ class LogFromStream(Extension):
42
kvps = {}
43
if log_item.kvps is not None and "reasoning" in log_item.kvps:
44
kvps["reasoning"] = log_item.kvps["reasoning"]
45
+
46
+ # step description for UI - using tool XY, writing Python code, etc.
47
+ if parsed is not None and "tool_name" in parsed and parsed["tool_name"]:
48
+ kvps["step"] = f"Using {parsed['tool_name']}..." # using tool XY
49
+ if parsed["tool_name"]=="code_execution_tool":
50
+ if "tool_args" in parsed and "runtime" in parsed["tool_args"]:
51
+ if parsed["tool_args"]["runtime"] == "python":
52
+ kvps["step"] = "Writing Python code..."
53
+ elif parsed["tool_args"]["runtime"] == "nodejs":
54
+ kvps["step"] = "Writing Node.js code..."
55
+ elif parsed["tool_args"]["runtime"] == "terminal":
56
+ kvps["step"] = "Writing terminal command..."
57
kvps.update(parsed)
58
59
+
60
+
61
# update the log item
62
log_item.update(heading=heading, content=text, kvps=kvps)
\ No newline at end of file
python/extensions/response_stream_end/_15_log_from_stream_end.py
new
+31
@@ -0,0 +1,31 @@
1
+from python.helpers import persist_chat, tokens
2
+from python.helpers.extension import Extension
3
+from agent import LoopData
4
+import asyncio
5
+from python.helpers.log import LogItem
6
+from python.helpers import log
7
+import math
8
+from python.extensions.before_main_llm_call._10_log_for_stream import build_heading, build_default_heading
9
+
10
+
11
+class LogFromStream(Extension):
12
+
13
+ async def execute(
14
+ self,
15
+ loop_data: LoopData = LoopData(),
16
+ text: str = "",
17
+ parsed: dict = {},
18
+ **kwargs,
19
+ ):
20
+
21
+ # get log item from loop data temporary params
22
+ log_item = loop_data.params_temporary["log_item_generating"]
23
+ if log_item is None:
24
+ return
25
+
26
+ # remove step parameter when done
27
+ if log_item.kvps is not None and "step" in log_item.kvps:
28
+ del log_item.kvps["step"]
29
+
30
+ # update the log item
31
+ log_item.update(kvps=log_item.kvps)
\ No newline at end of file
python/helpers/log.py
+1
@@ -21,6 +21,7 @@ Type = Literal[
21
"agent",
22
"browser",
23
"code_exe",
24
+ "subagent",
25
"error",
26
"hint",
27
"info",
python/tools/call_subordinate.py
+1
-1
@@ -45,7 +45,7 @@ class Delegation(Tool):
45
46
def get_log_object(self):
47
return self.agent.context.log.log(
48
- type="tool",
48
+ type="subagent",
49
heading=f"icon://communication {self.agent.agent_name}: Calling Subordinate Agent",
50
content="",
51
kvps=self.args,
webui/components/messages/process-group/process-group.css
+134
-202
@@ -88,7 +88,7 @@
88
flex: 0 1 auto;
89
font-size: var(--font-size-medium);
90
font-weight: 500;
91
- color: var(--color-text);
91
+ color: var(--color-text-muted);
92
opacity: 0.9;
93
white-space: nowrap;
94
overflow: hidden;
@@ -111,7 +111,7 @@
111
=========================================== */
112
113
/* Base status badge */
114
-.status-badge {
114
+.step-badge {
115
display: inline-flex;
116
align-items: center;
117
gap: 3px;
@@ -127,97 +127,132 @@
127
}
128
129
/* Status badge with icon */
130
-.status-badge .material-symbols-outlined {
130
+.step-badge .material-symbols-outlined {
131
font-size: 0.8rem;
132
line-height: 1;
133
}
134
135
/* Badge icon styling */
136
-.status-badge .badge-icon {
137
- font-size: 0.7rem;
136
+.step-badge .badge-icon {
137
+ font-size: var(--font-size-xs);
138
margin-right: 2px;
139
opacity: 0.9;
140
}
141
142
-/* Status colors - mapped from backend types via store */
142
+/* Status colors */
143
/* Each status defines --step-accent for cascading to internal icons */
144
145
/* GEN - agent type (blue/cyan) */
146
-.status-gen {
146
+.process-group .GEN {
147
--step-accent: #38bdf8;
148
}
149
+.light-mode .process-group .GEN {
150
+ --step-accent: #0284c7;
151
+}
152
153
/* END - response/done type (green) */
151
-.status-end {
154
+.process-group .END {
155
--step-accent: #22c55e;
156
}
157
+.light-mode .process-group .END {
158
+ --step-accent: #15803d;
159
+}
160
161
/* USE - tool usage (amber/yellow) */
156
-.status-tool {
162
+.process-group .USE {
163
--step-accent: #fbbf24;
164
}
165
+.light-mode .process-group .USE {
166
+ --step-accent: #b45309;
167
+}
168
169
/* MCP - mcp type (amber/yellow) */
161
-.status-mcp {
170
+.process-group .MCP {
171
--step-accent: #fbbf24;
172
}
173
+.light-mode .process-group .MCP {
174
+ --step-accent: #b45309;
175
+}
176
177
/* SUB - subagent type (teal) */
166
-.status-sub {
178
+.process-group .SUB {
179
--step-accent: #14b8a6;
180
}
181
+.light-mode .process-group .SUB {
182
+ --step-accent: #0f766e;
183
+}
184
185
/* EXE - code_exe type (magenta/purple) */
171
-.status-exe {
186
+.process-group .EXE {
187
--step-accent: #ba68c8;
188
}
189
+.light-mode .process-group .EXE {
190
+ --step-accent: #7c3aed;
191
+}
192
193
/* WWW - browser type (indigo) */
176
-.status-www {
194
+.process-group .WWW {
195
--step-accent: #818cf8;
196
}
197
+.light-mode .process-group .WWW {
198
+ --step-accent: #4f46e5;
199
+}
200
201
/* WAIT - progress type (slate) */
181
-.status-wait {
202
+.process-group .HDL {
203
--step-accent: #94a3b8;
204
}
205
+.light-mode .process-group .HDL {
206
+ --step-accent: #475569;
207
+}
208
209
/* INF - info type (gray) */
186
-.status-inf {
210
+.process-group .INF {
211
--step-accent: #94a3b8;
212
}
213
+.light-mode .process-group .INF {
214
+ --step-accent: #475569;
215
+}
216
217
/* HNT - hint type (yellow-green) */
191
-.status-hnt {
218
+.process-group .HNT {
219
--step-accent: #a3e635;
220
}
221
+.light-mode .process-group .HNT {
222
+ --step-accent: #65a30d;
223
+}
224
225
/* WRN - warning type (orange) */
196
-.status-wrn {
226
+.process-group .WRN {
227
--step-accent: #f97316;
228
}
229
+.light-mode .process-group .WRN {
230
+ --step-accent: #c2410c;
231
+}
232
+
233
234
/* ERR - error type (red) */
201
-.status-err {
235
+.process-group .ERR {
236
+ --step-accent: var(--color-error-text);
237
+}
238
+.light-mode .process-group .ERR {
239
--step-accent: var(--color-error-text);
240
}
241
242
/* UTL - util type (gray-blue) */
206
-.status-utl {
243
+.process-group .UTL {
244
--step-accent: #64748b;
245
}
209
-
210
-/* USR - user type (sky) */
211
-.status-usr {
212
- --step-accent: #38bdf8;
246
+.light-mode .process-group .UTL {
247
+ --step-accent: #334155;
248
}
249
250
216
-.process-step .status-badge{
251
+.process-group .step-badge{
252
color: var(--step-accent);
253
}
254
220
-.process-step .kvps-key{
255
+.process-group .kvps-key{
256
color: var(--step-accent);
257
}
258
@@ -255,7 +290,7 @@
290
}
291
292
.process-group-header .group-metrics .material-symbols-outlined {
258
- font-size: 0.75rem;
293
+ font-size: var(--font-size-xs);
294
opacity: 0.7;
295
}
296
@@ -287,7 +322,7 @@
322
}
323
324
.process-group-header .group-duration {
290
- font-size: 0.7rem;
325
+ font-size: var(--font-size-xs);
326
color: #81c784;
327
flex-shrink: 0;
328
font-family: var(--font-family-code);
@@ -298,7 +333,7 @@
333
334
/* Process Group Content - Animated expand/collapse */
335
.process-group-content {
301
- /* display: grid; */
336
+ display: grid;
337
grid-template-rows: 0fr;
338
opacity: 0;
339
margin-top: 0;
@@ -310,6 +345,7 @@
345
padding-top 0.25s ease-out,
346
border-color 0.2s ease-out;
347
overflow: hidden;
348
+ color: var(--color-text-muted);
349
}
350
351
.process-group-content > .process-steps {
@@ -317,7 +353,7 @@
353
}
354
355
.process-group.expanded .process-group-content {
320
- /* grid-template-rows: 1fr; */
356
+ grid-template-rows: 1fr;
357
opacity: 1;
358
margin-top: var(--spacing-xs);
359
padding-top: var(--spacing-xs);
@@ -374,7 +410,7 @@
410
.process-step-header .step-title {
411
flex: 1;
412
font-size: 0.8rem;
377
- color: var(--color-text);
413
+ /* color: var(--color-text-muted); */
414
white-space: nowrap;
415
overflow: hidden;
416
text-overflow: ellipsis;
@@ -407,7 +443,7 @@
443
444
/* Step Detail Content - Animated expand/collapse */
445
.process-step-detail {
410
- display: grid;
446
+ /* display: grid; */
447
grid-template-rows: 0fr;
448
opacity: 0;
449
margin-top: 0;
@@ -423,7 +459,7 @@
459
margin-top: 0;
460
}
461
426
-.process-step:not(.step-expanded) > .process-step-detail > .process-step-detail-content {
462
+.process-step:not(.step-expanded) > .process-step-detail > .process-step-detail-scroll {
463
padding: 0;
464
margin-top: 0;
465
max-height: 0;
@@ -436,36 +472,39 @@
472
height: 0;
473
}
474
439
-.process-step-detail-content::-webkit-scrollbar {
475
+.process-step-detail-scroll::-webkit-scrollbar {
476
display: none;
477
width: 0;
478
height: 0;
479
}
480
445
-.process-step-detail > .process-step-detail-content {
481
+.process-step-detail > .process-step-detail-scroll {
482
min-height: 0;
483
}
484
485
/* Use direct child selector (>) to prevent cascading to nested steps */
486
.process-step.step-expanded > .process-step-detail {
451
- grid-template-rows: 1fr;
487
+ /* grid-template-rows: 1fr; */
488
opacity: 1;
489
overflow: visible;
490
margin-top: var(--spacing-xs);
491
}
492
457
-.process-step.step-expanded > .process-step-detail > .process-step-detail-content {
458
- max-height: 350px;
493
+.process-step.step-expanded > .process-step-detail > .process-step-detail-scroll {
494
+ max-height: 40em;
495
overflow-y: auto;
496
}
497
462
-.process-step-detail-content {
498
+.process-step.step-expanded > .process-step-detail > .process-step-detail-scroll > .process-step-detail-scroll {
499
+}
500
+
501
+.process-step-detail-scroll {
502
transition: max-height 0.2s ease-out, padding 0.2s ease-out, margin-top 0.2s ease-out;
503
padding: var(--spacing-xs) 0;
504
margin-top: var(--spacing-xxs);
466
- margin-left: 28px; /* Align with icon */
505
+ /* margin-left: 28px; */
506
background-color: transparent;
468
- font-size: 0.7rem;
507
+ font-size: var(--font-size-xs);
508
line-height: 1.5;
509
overflow-y: auto;
510
-webkit-overflow-scrolling: touch; /* smooth scrolling on iOS */
@@ -474,29 +513,29 @@
513
overscroll-behavior-x: contain; /* avoid scroll chaining */
514
}
515
477
-.process-step-detail-content pre {
516
+/* .process-step-detail-scroll pre {
517
margin: 0;
518
white-space: pre-wrap;
519
word-break: break-word;
520
font-family: var(--font-family-code);
482
- font-size: 0.7rem;
521
+ font-size: var(--font-size-xs);
522
color: var(--color-text);
484
- /* opacity: 0.8; */
485
-}
523
+ opacity: 0.8;
524
+} */
525
526
/* KVPs in step detail - CSS Grid for aligned columns */
488
-.process-step-detail-content .step-kvps {
527
+.process-step-detail-scroll .step-kvps {
528
display: grid;
529
grid-template-columns: auto 1fr;
530
gap: var(--spacing-xs) var(--spacing-sm);
531
align-items: start;
532
}
533
495
-.process-step-detail-content .step-kvp {
534
+.process-step-detail-scroll .step-kvp {
535
display: contents; /* Children participate in parent grid */
536
}
537
499
-.process-step-detail-content .step-kvp-key {
538
+.process-step-detail-scroll .step-kvp-key {
539
color: var(--step-accent, var(--color-primary));
540
font-weight: 500;
541
opacity: 0.8;
@@ -505,13 +544,13 @@
544
justify-content: end;
545
}
546
508
-.process-step-detail-content .step-kvp-key .material-symbols-outlined {
547
+.process-step-detail-scroll .step-kvp-key .material-symbols-outlined {
548
font-size: 0.9rem;
549
color: var(--step-accent, var(--color-primary)) !important;
550
}
551
513
-.process-step-detail-content .step-kvp-value {
514
- color: var(--color-text);
552
+.process-step-detail-scroll .step-kvp-value {
553
+ /* color: var(--color-text); */
554
opacity: 0.85;
555
word-break: break-word;
556
white-space: pre-wrap;
@@ -519,111 +558,77 @@
558
line-height: 1.5;
559
}
560
522
-/* Thoughts styling - single icon with plain text */
523
-.process-step-detail-content .step-thoughts {
524
- display: flex;
525
- align-items: flex-start;
526
- gap: var(--spacing-sm);
527
- margin: var(--spacing-xs) 0;
528
-}
529
-
530
-.process-step-detail-content .thought-icon {
531
- font-size: 0.85rem;
532
- color: var(--step-accent, #38bdf8);
533
- flex-shrink: 0;
534
- margin-top: 2px;
535
-}
536
-
537
-.process-step-detail-content .thought-text {
538
- font-size: 0.7rem;
539
- font-weight: 300;
540
- color: var(--color-text);
541
- opacity: 0.65;
542
- line-height: 1.5;
543
- white-space: pre-wrap;
544
- word-break: break-word;
545
-}
546
-
547
-/* Light mode thoughts - uses step accent if available */
548
-.light-mode .process-step-detail-content .thought-icon {
549
- color: var(--step-accent, #b45309);
550
-}
561
562
/* Tool arguments - CSS Grid for aligned columns */
553
-.process-step-detail-content .step-tool-args {
563
+/* .process-step-detail-scroll .step-tool-args {
564
display: grid;
565
grid-template-columns: auto 1fr;
566
gap: 4px 8px;
567
margin: var(--spacing-xs) 0;
568
align-items: baseline;
559
-}
569
+} */
570
561
-.process-step-detail-content .tool-arg-row {
562
- display: contents; /* Children participate in parent grid */
563
-}
571
+/* .process-step-detail-scroll .tool-arg-row {
572
+ display: contents;
573
+} */
574
565
-.process-step-detail-content .tool-arg-label {
575
+/* .process-step-detail-scroll .tool-arg-label {
576
font-size: 0.68rem;
577
font-weight: 600;
578
color: var(--step-accent, var(--color-primary));
579
display: flex;
580
align-items: center;
581
justify-content: end;
572
-}
582
+} */
583
574
-.process-step-detail-content .tool-arg-label .material-symbols-outlined {
584
+/* .process-step-detail-scroll .tool-arg-label .material-symbols-outlined {
585
font-size: 0.9rem;
586
opacity: 0.85;
587
color: var(--step-accent, var(--color-primary));
578
-}
588
+} */
589
580
-.process-step-detail-content .tool-arg-value {
581
- font-size: 0.7rem;
590
+/* .process-step-detail-scroll .tool-arg-value {
591
+ font-size: var(--font-size-xs);
592
color: var(--color-text);
593
opacity: 0.85;
594
word-break: break-word;
595
font-family: var(--font-family-code);
586
-}
596
+} */
597
598
/* Light mode tool args - transparent like dark mode */
589
-.light-mode .process-step-detail-content .step-tool-args {
599
+/* .light-mode .process-step-detail-scroll .step-tool-args {
600
background: transparent;
601
border-left-color: transparent;
592
-}
602
+} */
603
594
-.process-step-detail-content .step-tool-header .tool-icon {
604
+/* .process-step-detail-scroll .step-tool-header .tool-icon {
605
font-size: 0.85rem;
606
color: var(--step-accent, var(--color-warning));
607
opacity: 0.85;
598
-}
608
+} */
609
600
-.process-step-detail-content .step-tool-header .tool-label {
610
+/* .process-step-detail-scroll .step-tool-header .tool-label {
611
font-size: 0.68rem;
612
font-weight: 600;
613
color: var(--step-accent, var(--color-warning));
614
opacity: 0.85;
605
-}
615
+} */
616
607
-.process-step-detail-content .step-tool-header .tool-name {
617
+/* .process-step-detail-scroll .step-tool-header .tool-name {
618
font-size: 0.72rem;
619
color: var(--color-text);
620
opacity: 0.85;
611
-}
621
+} */
622
613
-/* Terminal-style output for code_exe */
614
-.process-step-detail-content .step-terminal {
615
- margin: var(--spacing-xs) 0;
616
- font-family: var(--font-family-code);
617
- font-size: 0.72rem;
618
-}
623
620
-.process-step-detail-content .terminal-output {
624
+.process-step-detail .terminal-output {
625
margin: var(--spacing-xs) 0 0 0;
626
padding: var(--spacing-xs);
627
background: rgba(0, 0, 0, 0.8);
628
border-radius: 8px;
629
color: #c9d1d9;
630
white-space: pre;
631
+ font-family: monospace;
632
max-width: fit-content;
633
max-height: 300px;
634
overflow: auto;
@@ -633,36 +638,36 @@
638
overscroll-behavior-x: contain;
639
}
640
636
-.process-step-detail-content .terminal-output::-webkit-scrollbar {
641
+.process-step-detail .terminal-output::-webkit-scrollbar {
642
width: 4px;
643
height: 4px;
644
}
645
641
-.process-step-detail-content .terminal-output::-webkit-scrollbar-track {
646
+.process-step-detail .terminal-output::-webkit-scrollbar-track {
647
background: transparent;
648
}
649
645
-.process-step-detail-content .terminal-output::-webkit-scrollbar-thumb {
650
+.process-step-detail .terminal-output::-webkit-scrollbar-thumb {
651
background: rgba(255, 255, 255, 0.2);
652
border-radius: 2px;
653
}
654
650
-.process-step-detail-content .terminal-output::-webkit-scrollbar-thumb:hover {
655
+.process-step-detail .terminal-output::-webkit-scrollbar-thumb:hover {
656
background: rgba(255, 255, 255, 0.35);
657
}
658
659
/* Light mode terminal */
655
-.light-mode .process-step-detail-content .terminal-output {
660
+.light-mode .process-step-detail .terminal-output {
661
background: rgba(0, 0, 0, 0.05);
662
color: black;
663
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
664
}
665
661
-.light-mode .process-step-detail-content .terminal-output::-webkit-scrollbar-thumb {
666
+.light-mode .process-step-detail .terminal-output::-webkit-scrollbar-thumb {
667
background: rgba(0, 0, 0, 0.15);
668
}
669
665
-.light-mode .process-step-detail-content .terminal-output::-webkit-scrollbar-thumb:hover {
670
+.light-mode .process-step-detail .terminal-output::-webkit-scrollbar-thumb:hover {
671
background: rgba(0, 0, 0, 0.3);
672
}
673
@@ -671,87 +676,17 @@
676
border-top-color: rgba(0, 0, 0, 0.06);
677
}
678
674
-.light-mode .process-step-detail-content {
679
+.light-mode .process-step-detail-scroll {
680
background-color: transparent;
681
}
682
683
/* Light mode text colors for process group */
684
.light-mode .process-group-header .group-title,
685
.light-mode .process-step-header .step-title {
681
- color: var(--color-text);
686
+ color: var(--color-text-muted);
687
}
688
684
-/* Light mode status badge adjustments for better contrast */
685
-/* Override --step-accent for cascading to internal icons */
686
-.light-mode .status-gen {
687
- --step-accent: #0284c7;
688
- color: var(--step-accent);
689
-}
689
691
-.light-mode .status-end {
692
- --step-accent: #15803d;
693
- color: var(--step-accent);
694
-}
695
-
696
-.light-mode .status-tool {
697
- --step-accent: #b45309;
698
- color: var(--step-accent);
699
-}
700
-
701
-.light-mode .status-mcp {
702
- --step-accent: #b45309;
703
- color: var(--step-accent);
704
-}
705
-
706
-.light-mode .status-sub {
707
- --step-accent: #0f766e;
708
- color: var(--step-accent);
709
-}
710
-
711
-.light-mode .status-exe {
712
- --step-accent: #7c3aed;
713
- color: var(--step-accent);
714
-}
715
-
716
-.light-mode .status-www {
717
- --step-accent: #4f46e5;
718
- color: var(--step-accent);
719
-}
720
-
721
-.light-mode .status-wait {
722
- --step-accent: #475569;
723
- color: var(--step-accent);
724
-}
725
-
726
-.light-mode .status-inf {
727
- --step-accent: #475569;
728
- color: var(--step-accent);
729
-}
730
-
731
-.light-mode .status-hnt {
732
- --step-accent: #65a30d;
733
- color: var(--step-accent);
734
-}
735
-
736
-.light-mode .status-wrn {
737
- --step-accent: #c2410c;
738
- color: var(--step-accent);
739
-}
740
-
741
-.light-mode .status-err {
742
- --step-accent: var(--color-error-text);
743
- color: var(--step-accent);
744
-}
745
-
746
-.light-mode .status-utl {
747
- --step-accent: #334155;
748
- color: var(--step-accent);
749
-}
750
-
751
-.light-mode .status-usr {
752
- --step-accent: #0284c7;
753
- color: var(--step-accent);
754
-}
690
691
/* Animation for loading state */
692
@keyframes pulse-step {
@@ -769,10 +704,10 @@
704
}
705
706
.process-step-header .step-title {
772
- font-size: 0.7rem;
707
+ font-size: var(--font-size-xs);
708
}
709
775
- .process-step-detail-content {
710
+ .process-step-detail-scroll {
711
margin-left: 0;
712
}
713
}
@@ -782,20 +717,17 @@
717
These rules work with preferences-store toggles
718
=========================================== */
719
785
-/* Utility steps - default hidden (controlled by showUtils) */
786
-.process-step.message-util {
720
+/* .process-step.message-util {
721
display: none;
722
}
723
790
-/* Thoughts KVP row */
724
.step-kvp.msg-thoughts {
725
display: none;
726
}
727
795
-/* JSON pre content - default hidden */
796
-.process-step-detail-content pre.msg-json {
728
+.process-step-detail-scroll pre.msg-json {
729
display: none;
798
-}
730
+} */
731
732
/* Nested Process Steps (Subordinate Agents) */
733
@@ -830,7 +762,7 @@
762
}
763
764
/* Response content in process steps */
833
-.process-step-detail-content .step-response-content {
765
+.process-step-detail-scroll .step-response-content {
766
font-size: 0.75rem;
767
line-height: 1.6;
768
color: var(--color-text);
@@ -838,18 +770,18 @@
770
margin: var(--spacing-xs) 0;
771
}
772
841
-.process-step-detail-content .step-response-content p {
773
+.process-step-detail-scroll .step-response-content p {
774
margin: 0.5em 0;
775
}
776
845
-.process-step-detail-content .step-response-content ul,
846
-.process-step-detail-content .step-response-content ol {
777
+.process-step-detail-scroll .step-response-content ul,
778
+.process-step-detail-scroll .step-response-content ol {
779
margin: 0.5em 0;
780
padding-left: 1.5em;
781
}
782
783
/* Warning/error content in process steps */
852
-.process-step-detail-content .step-warning-content {
784
+.process-step-detail-scroll .step-warning-content {
785
font-size: 0.72rem;
786
line-height: 1.5;
787
color: var(--color-warning);
@@ -861,7 +793,7 @@
793
}
794
795
/* Browser screenshot content in process steps */
864
-.process-step-detail-content .screenshot-img {
796
+.process-step-detail-scroll .screenshot-img {
797
max-width: 100%;
798
max-height: 400px;
799
border-radius: 4px;
@@ -871,18 +803,18 @@
803
object-fit: contain;
804
}
805
874
-.process-step-detail-content .screenshot-img:hover {
806
+.process-step-detail-scroll .screenshot-img:hover {
807
transform: scale(1.02);
808
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
809
}
810
811
/* Light mode screenshot border */
880
-.light-mode .process-step-detail-content .screenshot-img {
812
+.light-mode .process-step-detail-scroll .screenshot-img {
813
border: 1px solid rgba(0, 0, 0, 0.15);
814
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
815
}
816
885
-.light-mode .process-step-detail-content .screenshot-img:hover {
817
+.light-mode .process-step-detail-scroll .screenshot-img:hover {
818
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
819
}
820
webui/components/modals/process-step-detail/process-step-detail.html
+1
-1
@@ -245,7 +245,7 @@
245
flex-wrap: wrap;
246
}
247
248
- .status-badge {
248
+ .step-badge {
249
padding: 0.25rem 0.5rem;
250
border-radius: 8px;
251
font-size: 0.75rem;
webui/components/sidebar/bottom/preferences/preferences-store.js
+10
-26
@@ -115,6 +115,14 @@ const model = {
115
this._detailMode = "current"; // Default
116
}
117
118
+ // load utility messages preference
119
+ try{
120
+ const storedShowUtils = localStorage.getItem("showUtils");
121
+ this._showUtils = storedShowUtils === "true";
122
+ } catch {
123
+ this._showUtils = false; // Default to speech off if localStorage is unavailable
124
+ }
125
+
126
// Apply all preferences
127
this._applyDarkMode(this._darkMode);
128
this._applyAutoScroll(this._autoScroll);
@@ -149,36 +157,12 @@ const model = {
157
158
159
_applyShowUtils(value) {
152
- // For original messages
160
+ localStorage.setItem("showUtils", value);
161
css.toggleCssProperty(
154
- ".message-util",
162
+ ".process-step.message-util",
163
"display",
164
value ? undefined : "none"
165
);
158
- // For process steps - toggle class on all existing elements
159
- const chatHistory = document.getElementById("chat-history");
160
- if (chatHistory) {
161
- const groups = chatHistory.children;
162
- for (let gi = groups.length - 1; gi >= 0; gi -= 1) {
163
- const messageGroup = groups[gi];
164
- const containers = messageGroup.children;
165
- for (let ci = containers.length - 1; ci >= 0; ci -= 1) {
166
- const container = containers[ci];
167
- if (!container.classList.contains("has-process-group")) continue;
168
- const processGroup = container.querySelector(".process-group");
169
- if (!processGroup) continue;
170
- const steps = processGroup.getElementsByClassName("process-step");
171
- for (let si = 0; si < steps.length; si += 1) {
172
- const step = steps[si];
173
- if (step.classList.contains("message-util")) {
174
- step.classList.toggle("show-util", value);
175
- }
176
- }
177
- }
178
- }
179
- }
180
- // Re-apply detail mode to reset current visible step
181
- applyModeSteps(this._detailMode, this._showUtils);
166
},
167
168
_applyChatWidth(value) {
webui/css/messages.css
+6
-4
@@ -72,8 +72,9 @@
72
73
.message.message-user {
74
text-align: end;
75
- margin-bottom: var(--spacing-md);
75
+ /* margin-bottom: var(--spacing-md); */
76
width: 100%;
77
+ margin-bottom: -2.5rem; /* compensate for action buttons */
78
}
79
80
.message-user .message-user-heading {
@@ -112,6 +113,7 @@
113
color: var(--color-text);
114
}
115
116
+
117
/* .light-mode .message-user .message-text pre {
118
color: #2e2e2e;
119
} */
@@ -511,9 +513,9 @@
513
/* Utility Classes */
514
.kvps-key {
515
font-weight: 500;
514
- font-size: var(--font-size-small);
516
+ font-size: var(--font-size-xs);
517
/* min-width: 7em; */
516
- max-width: 10em;
518
+ width: 4em;
519
text-align: right;
520
}
521
@@ -524,7 +526,7 @@
526
.kvps-val {
527
/* margin: 0.65rem 0 0.65rem 0.4rem; */
528
white-space: pre-wrap;
527
- font-size: var(--font-size-small);
529
+ font-size: var(--font-size-xs);
530
}
531
532
.kvps-img {
webui/index.css
+1
@@ -69,6 +69,7 @@
69
--spacing-lg: 2rem;
70
71
/* Font sizes */
72
+ --font-size-xs: 0.7rem;
73
--font-size-small: 0.8rem;
74
--font-size-smaller: 0.9rem;
75
--font-size-normal: 1rem;
webui/index.js
+6
-7
@@ -66,9 +66,9 @@ export async function sendMessage() {
66
: "";
67
68
// Render user message with attachments
69
- setMessage({ id: messageId, type: "user", heading, content: message, kvps: {
69
+ setMessages([{ id: messageId, type: "user", heading, content: message, kvps: {
70
// attachments: attachmentsWithUrls, // skip here, let the backend properly log them
71
- }});
71
+ }}]);
72
73
// sleep one frame to render the message before upload starts - better UX
74
sleep(0);
@@ -200,8 +200,8 @@ async function updateUserTime() {
200
updateUserTime();
201
setInterval(updateUserTime, 1000);
202
203
-function setMessage(...params) {
204
- const result = msgs.setMessage(...params);
203
+function setMessages(logs) {
204
+ const result = msgs.setMessages(logs);
205
const chatHistoryEl = document.getElementById("chat-history");
206
if (preferencesStore.autoScroll && chatHistoryEl) {
207
chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight;
@@ -316,9 +316,8 @@ export async function poll() {
316
317
if (lastLogVersion != response.log_version) {
318
updated = true;
319
- for (const log of response.logs) {
320
- setMessage(log);
321
- }
319
+ setMessages(response.logs);
320
+
321
afterMessagesUpdate(response.logs);
322
applyModeSteps(preferencesStore.detailMode, preferencesStore.showUtils);
323
}
webui/js/messages.js
+1382
-1139
@@ -15,65 +15,66 @@ const STEP_COLLAPSE_DELAY_MS = 3000;
15
// Delay before collapsing the last step when processing completes
16
const FINAL_STEP_COLLAPSE_DELAY_MS = 3000;
17
18
-// Tool-specific status codes (fallback for tool steps)
19
-const TOOL_STATUS_CODES = {
20
- call_subordinate: "SUB",
21
- search_engine: "WEB",
22
- a2a_chat: "A2A",
23
- behaviour_adjustment: "ADJ",
24
- document_query: "DOC",
25
- vision_load: "EYE",
26
- notify_user: "NTF",
27
- scheduler: "SCH",
28
- unknown: "UNK",
29
- memory_save: "MEM",
30
- memory_load: "MEM",
31
- memory_forget: "MEM",
32
- memory_delete: "MEM"
33
-};
18
+// // Tool-specific status codes (fallback for tool steps)
19
+// const TOOL_STATUS_CODES = {
20
+// call_subordinate: "SUB",
21
+// search_engine: "WEB",
22
+// a2a_chat: "A2A",
23
+// behaviour_adjustment: "ADJ",
24
+// document_query: "DOC",
25
+// vision_load: "EYE",
26
+// notify_user: "NTF",
27
+// scheduler: "SCH",
28
+// unknown: "UNK",
29
+// memory_save: "MEM",
30
+// memory_load: "MEM",
31
+// memory_forget: "MEM",
32
+// memory_delete: "MEM",
33
+// };
34
35
// Tool-specific status classes (fallback for tool steps)
36
-const TOOL_STATUS_CLASSES = {
37
- call_subordinate: "status-sub"
38
-};
36
+// const TOOL_STATUS_CLASSES = {
37
+// call_subordinate: "status-sub",
38
+// };
39
40
const TYPE_STATUS_CODES = {
41
- agent: "GEN",
41
+ // agent: "GEN",
42
response: "END",
43
- tool: "USE",
44
- code_exe: "EXE",
45
- browser: "WWW",
46
- progress: "HLD",
47
- mcp: "MCP",
48
- subagent: "SUB",
43
+ // tool: "USE",
44
+ // code_exe: "EXE",
45
+ // browser: "WWW",
46
+ // progress: "HLD",
47
+ // mcp: "MCP",
48
+ // subagent: "SUB",
49
info: "INF",
50
hint: "HNT",
51
- warning: "WRN",
51
+ // warning: "WRN",
52
rate_limit: "WRN",
53
- error: "ERR",
54
- util: "UTL",
55
- done: "END"
53
+ // error: "ERR",
54
+ // util: "UTL",
55
+ done: "END",
56
};
57
58
const TYPE_STATUS_CLASSES = {
59
- agent: "status-gen",
59
+ // agent: "status-gen",
60
response: "status-end",
61
- tool: "status-tool",
62
- code_exe: "status-exe",
63
- browser: "status-www",
64
- progress: "status-wait",
65
- mcp: "status-mcp",
66
- subagent: "status-sub",
61
+ // tool: "status-tool",
62
+ // code_exe: "status-exe",
63
+ // browser: "status-www",
64
+ // progress: "status-wait",
65
+ // mcp: "status-mcp",
66
+ // subagent: "status-sub",
67
info: "status-inf",
68
hint: "status-hnt",
69
- warning: "status-wrn",
69
+ // warning: "status-wrn",
70
rate_limit: "status-wrn",
71
- error: "status-err",
72
- util: "status-utl",
73
- done: "status-end"
71
+ // error: "status-err",
72
+ // util: "status-utl",
73
+ done: "status-end",
74
};
75
76
-let chatHistory = null;
76
+let _chatHistory = null;
77
+let _massRender = false;
78
79
// handlers for log message rendering
80
export function getMessageHandler(type) {
@@ -113,49 +114,62 @@ export function getMessageHandler(type) {
114
}
115
}
116
116
-
117
/**
118
* Mark a process group as the active one (via .active class)
119
*/
120
function setActiveProcessGroup(group) {
121
if (!group) return;
122
-
122
+
123
// Already active? Nothing to do
124
if (group.classList.contains("active")) return;
125
-
125
+
126
// Clear active + shiny from all other groups
127
- getChatHistoryEl().querySelectorAll(".process-group.active").forEach(g => {
128
- if (g !== group) {
129
- g.classList.remove("active");
130
- g.querySelectorAll(".step-title.shiny-text").forEach(el => el.classList.remove("shiny-text"));
131
- }
132
- });
133
-
127
+ getChatHistoryEl()
128
+ .querySelectorAll(".process-group.active")
129
+ .forEach((g) => {
130
+ if (g !== group) {
131
+ g.classList.remove("active");
132
+ g.querySelectorAll(".step-title.shiny-text").forEach((el) =>
133
+ el.classList.remove("shiny-text"),
134
+ );
135
+ }
136
+ });
137
+
138
// Mark this group as active
139
group.classList.add("active");
140
}
141
142
export function clearActiveStepShine() {
143
// Clear all shiny step titles in process steps
140
- getChatHistoryEl().querySelectorAll(".process-step .step-title.shiny-text").forEach((el) => {
141
- el.classList.remove("shiny-text");
142
- });
144
+ getChatHistoryEl()
145
+ .querySelectorAll(".process-step .step-title.shiny-text")
146
+ .forEach((el) => {
147
+ el.classList.remove("shiny-text");
148
+ });
149
}
150
151
function getChatHistoryEl() {
146
- if(!chatHistory) chatHistory = document.getElementById("chat-history");
147
- return chatHistory;
152
+ if (!_chatHistory) _chatHistory = document.getElementById("chat-history");
153
+ return _chatHistory;
154
}
155
150
-function getLastMessageContainer() {
151
- const chatHistoryEl = getChatHistoryEl();
152
- if (!chatHistoryEl) return null;
153
- const lastGroup = chatHistoryEl.lastElementChild;
154
- if (!lastGroup) return null;
155
- return lastGroup.lastElementChild;
156
+function getLastMessageGroup() {
157
+ return getChatHistoryEl()?.lastElementChild;
158
}
159
158
-function appendToMessageGroup(messageContainer, position, forceNewGroup = false) {
160
+// function getLastMessageContainer() {
161
+// const chatHistoryEl = getChatHistoryEl();
162
+// if (!chatHistoryEl) return null;
163
+// const lastGroup = chatHistoryEl.lastElementChild;
164
+// if (!lastGroup) return null;
165
+// return lastGroup.lastElementChild;
166
+// }
167
+
168
+function appendToMessageGroup(
169
+ messageContainer,
170
+ position,
171
+ forceNewGroup = false,
172
+) {
173
const chatHistoryEl = getChatHistoryEl();
174
if (!chatHistoryEl) return;
175
@@ -164,75 +178,108 @@ function appendToMessageGroup(messageContainer, position, forceNewGroup = false)
178
179
if (!forceNewGroup && lastGroup && lastGroupType === position) {
180
lastGroup.appendChild(messageContainer);
167
- return;
181
+ } else {
182
+ const group = document.createElement("div");
183
+ group.classList.add("message-group", `message-group-${position}`);
184
+ group.setAttribute("data-group-type", position);
185
+ group.appendChild(messageContainer);
186
+ chatHistoryEl.appendChild(group);
187
}
169
-
170
- const group = document.createElement("div");
171
- group.classList.add("message-group", `message-group-${position}`);
172
- group.setAttribute("data-group-type", position);
173
- group.appendChild(messageContainer);
174
- chatHistoryEl.appendChild(group);
188
}
189
177
-function getStatusCode(type, toolName = null) {
178
- if (type === "tool" && toolName && TOOL_STATUS_CODES[toolName]) {
179
- return TOOL_STATUS_CODES[toolName];
180
- }
181
- return TYPE_STATUS_CODES[type] || type?.toUpperCase()?.slice(0, 4) || "GEN";
182
-}
190
+// function getStatusCode(type, toolName = null) {
191
+// if (type === "tool" && toolName && TOOL_STATUS_CODES[toolName]) {
192
+// return TOOL_STATUS_CODES[toolName];
193
+// }
194
+// return TYPE_STATUS_CODES[type] || type?.toUpperCase()?.slice(0, 4) || "GEN";
195
+// }
196
+
197
+// function getStatusClass(type, toolName = null) {
198
+// if (type === "tool" && toolName && TOOL_STATUS_CLASSES[toolName]) {
199
+// return TOOL_STATUS_CLASSES[toolName];
200
+// }
201
+// return TYPE_STATUS_CLASSES[type] || "status-gen";
202
+// }
203
+
204
+// /**
205
+// * Resolve tool name from kvps, existing attribute, or previous siblings
206
+// * For 'tool' type steps, inherits from preceding step if not directly available
207
+// */
208
+// function resolveToolName(type, kvps, stepElement) {
209
+// // Direct from kvps
210
+// if (kvps?.tool_name) return kvps.tool_name;
211
+
212
+// // Keep existing if present (for non-tool types during updates)
213
+// if (type !== "tool" && stepElement?.hasAttribute("data-tool-name")) {
214
+// return stepElement.getAttribute("data-tool-name");
215
+// }
216
+
217
+// // // Inherit from previous sibling (for tool steps)
218
+// // if (type === 'tool' && stepElement) {
219
+// // let prev = stepElement.previousElementSibling;
220
+// // while (prev) {
221
+// // if (prev.hasAttribute('data-tool-name')) {
222
+// // return prev.getAttribute('data-tool-name');
223
+// // }
224
+// // prev = prev.previousElementSibling;
225
+// // }
226
+// // }
227
+
228
+// return null;
229
+// }
230
184
-function getStatusClass(type, toolName = null) {
185
- if (type === "tool" && toolName && TOOL_STATUS_CLASSES[toolName]) {
186
- return TOOL_STATUS_CLASSES[toolName];
187
- }
188
- return TYPE_STATUS_CLASSES[type] || "status-gen";
189
-}
231
+// entrypoint called from poll/WS communication, this is how all messages are rendered and updated
232
+// input is raw log format
233
+export function setMessages(messages) {
234
+ // set _massRender flag for handlers to know how to behave
235
+ const history = getChatHistoryEl();
236
+ const historyEmpty = !history || history.childElementCount === 0;
237
+ const isLargeAppend = !historyEmpty && messages.length > 10;
238
+ const cutoff = isLargeAppend ? Math.max(0, messages.length - 2) : 0;
239
191
-/**
192
- * Resolve tool name from kvps, existing attribute, or previous siblings
193
- * For 'tool' type steps, inherits from preceding step if not directly available
194
- */
195
-function resolveToolName(type, kvps, stepElement) {
196
- // Direct from kvps
197
- if (kvps?.tool_name) return kvps.tool_name;
198
-
199
- // Keep existing if present (for non-tool types during updates)
200
- if (type !== 'tool' && stepElement?.hasAttribute('data-tool-name')) {
201
- return stepElement.getAttribute('data-tool-name');
240
+ // process messages
241
+ for (let i = 0; i < messages.length; i++) {
242
+ _massRender = historyEmpty || (isLargeAppend && i < cutoff);
243
+ setMessage(messages[i]);
244
}
203
-
204
- // // Inherit from previous sibling (for tool steps)
205
- // if (type === 'tool' && stepElement) {
206
- // let prev = stepElement.previousElementSibling;
207
- // while (prev) {
208
- // if (prev.hasAttribute('data-tool-name')) {
209
- // return prev.getAttribute('data-tool-name');
210
- // }
211
- // prev = prev.previousElementSibling;
212
- // }
213
- // }
214
-
215
- return null;
216
-}
245
218
-/**
219
- * Update status badge text content
220
- */
221
-function updateBadgeText(badge, newCode) {
222
- if (!badge) return;
223
- badge.textContent = newCode;
246
+ // reset _massRender flag
247
+ _massRender = false;
248
}
249
226
-
250
// entrypoint called from poll/WS communication, this is how all messages are rendered and updated
251
// input is raw log format
229
-export function setMessage({ no, id, type, heading, content, kvps, timestamp, agentno, ...additional }) {
252
+export function setMessage({
253
+ no,
254
+ id,
255
+ type,
256
+ heading,
257
+ content,
258
+ kvps,
259
+ timestamp,
260
+ agentno,
261
+ ...additional
262
+}) {
263
const handler = getMessageHandler(type);
231
- // prefer log ID if set to match user message created on frontend with backend updates
232
- return handler({ id: id || no, type, heading, content, kvps, timestamp, agentno, ...additional });
264
+ // prefer log ID if set to match user message created on frontend with backend updates
265
+ return handler({
266
+ id: id || no,
267
+ type,
268
+ heading,
269
+ content,
270
+ kvps,
271
+ timestamp,
272
+ agentno,
273
+ ...additional,
274
+ });
275
}
276
235
-function getOrCreateMessageContainer(id, position, containerClasses = [], forceNewGroup = false) {
277
+function getOrCreateMessageContainer(
278
+ id,
279
+ position,
280
+ containerClasses = [],
281
+ forceNewGroup = false,
282
+) {
283
let container = document.getElementById(`message-${id}`);
284
if (!container) {
285
container = document.createElement("div");
@@ -251,27 +298,35 @@ function getOrCreateMessageContainer(id, position, containerClasses = [], forceN
298
return container;
299
}
300
254
-function getLastProcessGroup() {
255
- const lastContainer = getLastMessageContainer();
301
+function getLastProcessGroup(allowCompleted = true) {
302
+ const lastContainer = getLastMessageGroup();
303
if (!lastContainer) return null;
257
- if (!lastContainer.classList.contains("has-process-group")) return null;
258
- const group = lastContainer.querySelector(".process-group");
259
- if (!group || group.classList.contains("process-group-completed")) {
304
+ const groups = lastContainer.querySelectorAll(".process-group");
305
+ if (groups.length === 0) return null;
306
+ const group = groups[groups.length - 1];
307
+ if (!allowCompleted && group.classList.contains("process-group-completed"))
308
return null;
261
- }
309
+
310
return group;
311
}
312
265
-function getOrCreateProcessGroup(id) {
266
- const existing = getLastProcessGroup();
267
- if (existing) {
268
- setActiveProcessGroup(existing);
269
- return existing;
270
- }
313
+function getOrCreateProcessGroup(id, allowCompleted = true) {
314
+ // first try direct match by ID
315
+ const byId = document.getElementById(`process-group-${id}`);
316
+ if (byId) return byId;
317
318
+ // if not found, try to find the last process group
319
+ const existing = getLastProcessGroup(allowCompleted);
320
+ if (existing) return existing;
321
+
322
+ // lastly create new
323
const messageContainer = document.createElement("div");
273
- messageContainer.id = `message-${id}`;
274
- messageContainer.classList.add("message-container", "ai-container", "has-process-group");
324
+ messageContainer.id = `process-group-${id}`;
325
+ messageContainer.classList.add(
326
+ "message-container",
327
+ "ai-container",
328
+ "has-process-group",
329
+ );
330
331
const group = createProcessGroup(id);
332
group.classList.add("embedded");
@@ -293,7 +348,7 @@ function buildDetailPayload(stepData) {
348
agentno: stepData.agentno,
349
toolName: stepData.toolName,
350
statusCode: stepData.statusCode,
296
- statusClass: stepData.statusClass
351
+ statusClass: stepData.statusClass,
352
};
353
}
354
@@ -304,8 +359,12 @@ function buildStepCopyContent(stepData) {
359
if (stepData.content) parts.push(stepData.content);
360
if (stepData.kvps) {
361
for (const [key, value] of Object.entries(stepData.kvps)) {
307
- if (key === "reasoning" || key === "finished" || key === "attachments") continue;
308
- const valStr = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
362
+ if (key === "reasoning" || key === "finished" || key === "attachments")
363
+ continue;
364
+ const valStr =
365
+ typeof value === "object"
366
+ ? JSON.stringify(value, null, 2)
367
+ : String(value);
368
parts.push(`${key}: ${valStr}`);
369
}
370
}
@@ -315,48 +374,281 @@ function buildStepCopyContent(stepData) {
374
function drawProcessStep({
375
id,
376
title,
318
- statusClass,
319
- statusCode,
320
- kvps = null,
321
- detailHandler = null,
322
- copyContent = null,
323
- speakContent = null,
324
- type = "agent",
325
- heading = null,
326
- content = null,
327
- timestamp = null,
328
- agentno = 0,
329
- toolName = null,
330
- detailPayload = null,
377
+ code,
378
+ classes,
379
+ kvps,
380
+ content,
381
+ contentClasses,
382
+ copyText,
383
+ speakText,
384
+ log,
385
+ allowCompletedGroup = false,
386
...additional
387
}) {
333
-
334
- const group = getOrCreateProcessGroup(id);
388
+ // group and steps DOM elements
389
+ const group = getOrCreateProcessGroup(id, allowCompletedGroup);
390
+ const stepsContainer = group.querySelector(".process-steps");
391
const stepId = `process-step-${id}`;
336
- const stepData = {
337
- id,
338
- type,
339
- title,
340
- heading,
341
- content,
342
- kvps,
343
- timestamp,
344
- agentno,
345
- toolName,
346
- statusCode,
347
- statusClass
348
- };
349
-
392
let step = document.getElementById(stepId);
351
- const detailData = detailPayload || buildDetailPayload(stepData);
352
- const copyText = copyContent ?? buildStepCopyContent(stepData);
353
- const speakText = speakContent ?? copyText;
393
355
- if (step)
356
- updateProcessStep(step, stepData, detailData, copyText, speakText, detailHandler);
357
- else
358
- step = addProcessStep(group, stepData, detailData, copyText, speakText, detailHandler);
359
- return step;
394
+ const isNewStep = !step;
395
+ const isGroupCompleted = group.classList.contains("process-group-completed");
396
+
397
+ // const detailData = buildDetailPayload(stepData);
398
+ // const speakText = speakContent ?? copyText;
399
+ // const speakText = speakContent ?? copyText;
400
+
401
+ if (isNewStep) {
402
+ // create the base DOM element for the step
403
+ step = document.createElement("div");
404
+ step.id = stepId;
405
+ step.classList.add("process-step");
406
+
407
+ // set data attributes of the step
408
+ step.setAttribute("data-log-type", log.type);
409
+ step.setAttribute("data-step-id", id);
410
+ step.setAttribute("data-agent-number", log.agentno);
411
+
412
+ // apply step classes
413
+ if (classes) step.classList.add(...classes);
414
+
415
+ // if (toolName) {
416
+ // step.setAttribute("data-tool-name", toolName);
417
+ // }
418
+ // timestamp data
419
+ // if (log.timestamp) {
420
+ // step.setAttribute("data-timestamp", log.timestamp);
421
+
422
+ // if (!group.getAttribute("data-start-timestamp")) {
423
+ // group.setAttribute("data-start-timestamp", log.timestamp);
424
+ // const timeMetricEl = group.querySelector(".metric-time .metric-value");
425
+ // if (timeMetricEl) {
426
+ // const date = new Date(parseFloat(timestamp) * 1000);
427
+ // const hours = String(date.getHours()).padStart(2, "0");
428
+ // const minutes = String(date.getMinutes()).padStart(2, "0");
429
+ // timeMetricEl.textContent = `${hours}:${minutes}`;
430
+ // }
431
+ // }
432
+ // }
433
+
434
+ // // create step detail container
435
+ // detail = document.createElement("div");
436
+ // detail.classList.add("process-step-detail");
437
+ // detailContent = document.createElement("div")
438
+ // detailContent.classList.add("process-step-detail-content")
439
+
440
+ // const stepActionBtns = document.createElement("div");
441
+ // stepActionBtns.classList.add("step-detail-actions");
442
+ // detail.appendChild(stepActionBtns);
443
+ // step.appendChild(detail);
444
+
445
+ let appendTarget = stepsContainer;
446
+ const parentStep = findParentDelegationStep(group, log.agentno);
447
+ if (parentStep) {
448
+ appendTarget = getNestedContainer(parentStep);
449
+ step.classList.add("nested-step");
450
+ }
451
+
452
+ // remove any existing shiny-text from group
453
+ group
454
+ .querySelectorAll(".process-step .step-title.shiny-text")
455
+ .forEach((el) => {
456
+ el.classList.remove("shiny-text");
457
+ });
458
+
459
+ // insert step
460
+ appendTarget.appendChild(step);
461
+
462
+ // add interaction handlers - don't collapse when user interacts
463
+ addStepCollapseInteractionHandlers(step); // TODO? cleanup listeners?
464
+
465
+ // expand all or current step based on settings
466
+ const detailMode = preferencesStore.detailMode;
467
+ const isActiveGroup = group.classList.contains("active");
468
+
469
+ //expand all
470
+ if (detailMode === "expanded") {
471
+ step.classList.add("step-expanded");
472
+ // expand current step and schedule collapse of previous
473
+ } else if (detailMode === "current") {
474
+ if (isActiveGroup && !isGroupCompleted) {
475
+ step.classList.add("step-expanded");
476
+ const allExpandedSteps = stepsContainer.querySelectorAll(
477
+ ".process-step.step-expanded",
478
+ );
479
+ allExpandedSteps.forEach((expandedStep) => {
480
+ if (expandedStep.id !== stepId) {
481
+ scheduleStepCollapse(expandedStep, STEP_COLLAPSE_DELAY_MS);
482
+ }
483
+ });
484
+ }
485
+ }
486
+ }
487
+
488
+ // is step expanded?
489
+ const isExpanded = step.classList.contains("step-expanded");
490
+
491
+ // create step header
492
+ const stepHeader = ensureChild(
493
+ step,
494
+ ".process-step-header",
495
+ "div",
496
+ "process-step-header",
497
+ );
498
+
499
+ // create step detail
500
+ const stepDetail = ensureChild(
501
+ step,
502
+ ".process-step-detail",
503
+ "div",
504
+ "process-step-detail",
505
+ );
506
+ const stepDetailScroll = ensureChild(
507
+ stepDetail,
508
+ ".process-step-detail-scroll",
509
+ "div",
510
+ "process-step-detail-scroll",
511
+ );
512
+
513
+ // create action buttons
514
+ const stepActionBtns = ensureChild(
515
+ stepDetail,
516
+ ".step-detail-actions",
517
+ "div",
518
+ "step-detail-actions",
519
+ );
520
+
521
+ // else {
522
+ // if (timestamp && !step.hasAttribute("data-timestamp")) {
523
+ // step.setAttribute("data-timestamp", timestamp);
524
+ // }
525
+ // if (agentno !== undefined) {
526
+ // step.setAttribute("data-agent-number", agentno);
527
+ // }
528
+ // }
529
+
530
+ // const toolNameToUse = resolveToolName(type, kvps, step) || toolName;
531
+ // if (toolNameToUse) {
532
+ // step.setAttribute("data-tool-name", toolNameToUse);
533
+ // }
534
+
535
+ if (!stepHeader.hasAttribute("data-expand-handler")) {
536
+ stepHeader.setAttribute("data-expand-handler", "true");
537
+ stepHeader.addEventListener("click", (e) => {
538
+ e.stopPropagation();
539
+ cancelStepCollapse(step);
540
+ step.classList.toggle("step-expanded");
541
+ if (step.classList.contains("step-expanded")) {
542
+ step.setAttribute("data-user-pinned", "true");
543
+ } else {
544
+ step.removeAttribute("data-user-pinned");
545
+ }
546
+ });
547
+ }
548
+
549
+ // header row - expand icon
550
+ ensureChild(stepHeader, ".step-expand-icon", "span", "step-expand-icon");
551
+
552
+ // header row - status badge
553
+ const badge = ensureChild(stepHeader, ".step-badge", "span", "step-badge");
554
+
555
+ // set code class if changed
556
+ const prevCode = step.getAttribute("data-step-code");
557
+ if (prevCode !== code) {
558
+ if (prevCode) step.classList.remove(prevCode);
559
+ step.setAttribute("data-step-code", code);
560
+ step.classList.add(code);
561
+ step.querySelector(".step-badge").textContent = code;
562
+ badge.innerText = code;
563
+ }
564
+
565
+ // header row - title
566
+ const titleEl = ensureChild(stepHeader, ".step-title", "span", "step-title");
567
+ titleEl.textContent = title;
568
+
569
+ // const detail = ensureChild(step, ".process-step-detail", "div", "process-step-detail");
570
+ // const detailContent = ensureChild(
571
+ // detail,
572
+ // ".process-step-detail-content",
573
+ // "div",
574
+ // "process-step-detail-content",
575
+ // );
576
+
577
+ // let skipFullRender = false;
578
+
579
+ // const terminal = stepDetailContent.querySelector(".terminal-output");
580
+ // const scroller = terminal ? new Scroller(terminal) : null;
581
+
582
+ // if (type === "browser" && kvps?.screenshot) {
583
+ // const existingImg = detailContent.querySelector(".screenshot-img");
584
+ // const newSrc = kvps.screenshot.replace("img://", "/image_get?path=");
585
+ // if (existingImg) {
586
+ // if (!existingImg.src.endsWith(newSrc.split("?path=")[1])) {
587
+ // existingImg.src = newSrc;
588
+ // }
589
+ // skipFullRender = true;
590
+ // }
591
+ // }
592
+
593
+ // if (!skipFullRender) {
594
+ // renderStepDetailContent(detailContent, content, kvps, type);
595
+
596
+ // const newTerminal = detailContent.querySelector(".terminal-output");
597
+ // if (newTerminal && scroller?.wasAtBottom) {
598
+ // newTerminal.scrollTop = newTerminal.scrollHeight;
599
+ // }
600
+ // }
601
+
602
+ const detailScroller = new Scroller(stepDetailScroll); // scroller for step detail content
603
+
604
+ // update KVPs of the step detail
605
+ const kvpsTable = drawKvpsIncremental(stepDetailScroll, kvps);
606
+
607
+ // update content
608
+ const stepDetailContent = ensureChild(
609
+ stepDetailScroll,
610
+ ".process-step-detail-content",
611
+ "p",
612
+ "process-step-detail-content",
613
+ ...(contentClasses || []),
614
+ );
615
+ stepDetailContent.textContent = content;
616
+
617
+ // const detailDataToUse =
618
+ // detailData ||
619
+ // buildDetailPayload({
620
+ // ...stepData,
621
+ // toolName: toolNameToUse,
622
+ // statusCode: resolvedStatusCode,
623
+ // statusClass: resolvedStatusClass,
624
+ // });
625
+
626
+ // const stepActions = ensureChild(detail, ".step-detail-actions", "div", "step-detail-actions");
627
+ addActionButtonsToElement(stepActionBtns, {
628
+ detailPayload: {}, // detailDataToUse,
629
+ onViewDetails: null,
630
+ copyContent: copyText,
631
+ speakContent: speakText,
632
+ });
633
+
634
+ if (isExpanded && !isMassRender()) detailScroller.reApplyScroll(); // reapply scroll position (autoscroll if bottom) - only when expanded already and not
635
+
636
+ updateProcessGroupHeader(group);
637
+
638
+ if (isNewStep && !isGroupCompleted) {
639
+ titleEl.classList.add("shiny-text");
640
+ }
641
+
642
+ // return anything useful
643
+ return {
644
+ step,
645
+ detail: stepDetail,
646
+ content: stepDetailContent,
647
+ contentScroller: detailScroller,
648
+ kvpsTable,
649
+ actionButtons: stepActionBtns,
650
+ isExpanded,
651
+ };
652
}
653
654
function drawStandaloneMessage(id, heading, content, options = {}) {
@@ -371,22 +663,39 @@ function drawStandaloneMessage(id, heading, content, options = {}) {
663
latex = false,
664
kvps = null,
665
copyContent = null,
374
- speakContent = null
666
+ speakContent = null,
667
} = options;
668
377
- const container = getOrCreateMessageContainer(id, position, containerClasses, forceNewGroup);
378
- const messageDiv = _drawMessage(container, heading, content, kvps, messageClasses, contentClasses, markdown, latex, mainClass);
669
+ const container = getOrCreateMessageContainer(
670
+ id,
671
+ position,
672
+ containerClasses,
673
+ forceNewGroup,
674
+ );
675
+ const messageDiv = _drawMessage({
676
+ messageContainer: container,
677
+ heading,
678
+ content,
679
+ kvps,
680
+ messageClasses,
681
+ contentClasses,
682
+ markdown,
683
+ latex,
684
+ mainClass,
685
+ });
686
687
const copyText = copyContent ?? content ?? "";
688
const speakText = speakContent ?? copyText;
382
- addActionButtonsToElement(messageDiv, { copyContent: copyText, speakContent: speakText });
689
+ addActionButtonsToElement(messageDiv, {
690
+ copyContent: copyText,
691
+ speakContent: speakText,
692
+ });
693
694
return container;
695
}
696
387
-
697
// draw a message with a specific type
389
-export function _drawMessage(
698
+export function _drawMessage({
699
messageContainer,
700
heading,
701
content,
@@ -395,8 +704,8 @@ export function _drawMessage(
704
contentClasses = [],
705
markdown = false,
706
latex = false,
398
- mainClass = ""
399
-) {
707
+ mainClass = "",
708
+}) {
709
// Find existing message div or create new one
710
let messageDiv = messageContainer.querySelector(".message");
711
if (!messageDiv) {
@@ -481,7 +790,6 @@ export function _drawMessage(
790
}
791
792
adjustMarkdownRender(contentDiv);
484
-
793
} else {
794
let preElement = bodyDiv.querySelector(".msg-content");
795
if (!preElement) {
@@ -502,7 +810,6 @@ export function _drawMessage(
810
}
811
812
spanElement.innerHTML = convertHTML(content);
505
-
813
}
814
} else {
815
// Remove content if it exists but content is empty
@@ -543,40 +850,62 @@ export function addBlankTargetsToLinks(str) {
850
return doc.body.innerHTML;
851
}
852
546
-export function drawMessageDefault({ id, heading, content, kvps = null, ...additional }) {
853
+export function drawMessageDefault({
854
+ id,
855
+ heading,
856
+ content,
857
+ kvps = null,
858
+ ...additional
859
+}) {
860
return drawStandaloneMessage(id, heading, content, {
861
position: "left",
862
containerClasses: ["ai-container"],
863
mainClass: "message-default",
864
messageClasses: ["message-ai"],
865
contentClasses: ["msg-json"],
553
- kvps
866
+ kvps,
867
});
868
}
869
557
-export function drawMessageAgent({ id, type, heading, content, kvps = null, timestamp = null, agentno = 0, ...additional }) {
558
- const title = getStepTitle(heading, kvps, type);
559
- const statusCode = getStatusCode(type);
560
- const statusClass = getStatusClass(type);
561
- const toolName = kvps?.tool_name || null;
562
- let displayKvps = { "icon://lightbulb": kvps?.thoughts, "icon://step": heading, "icon school":"dummy" }
870
+export function drawMessageAgent({
871
+ id,
872
+ type,
873
+ heading,
874
+ content,
875
+ kvps = null,
876
+ timestamp = null,
877
+ agentno = 0,
878
+ ...additional
879
+}) {
880
+ const title = cleanStepTitle(heading);
881
+ let displayKvps = {};
882
+ if (kvps?.thoughts) displayKvps["icon://lightbulb"] = kvps.thoughts;
883
+ if (kvps?.step) displayKvps["icon://step"] = kvps.step;
884
885
return drawProcessStep({
886
id,
887
title,
567
- statusClass,
568
- statusCode,
888
+ code: "GEN",
889
+ codeClass: "status-gen",
890
+ classes: null,
891
kvps: displayKvps,
570
- type,
571
- heading,
572
- content,
573
- timestamp,
574
- agentno,
575
- toolName
892
+ copyText: kvps?.thoughts,
893
+ speakText: kvps?.thoughts,
894
+ log: arguments[0],
895
});
896
}
897
579
-export function drawMessageResponse({ id, type, heading, content, kvps = null, timestamp = null, agentno = 0, ...additional }) {
898
+export function drawMessageResponse({
899
+ id,
900
+ type,
901
+ heading,
902
+ content,
903
+ kvps = null,
904
+ timestamp = null,
905
+ agentno = 0,
906
+ ...additional
907
+}) {
908
+ // response of subordinate agent - render as process step
909
if (agentno && agentno > 0) {
910
const title = getStepTitle(heading, kvps, type);
911
const statusCode = getStatusCode(type);
@@ -591,29 +920,61 @@ export function drawMessageResponse({ id, type, heading, content, kvps = null, t
920
heading,
921
content,
922
timestamp,
594
- agentno
923
+ agentno,
924
});
925
}
926
927
+ // response of agent 0, render as response to user
928
+ // get last process group or create new container (if first message)
929
const group = getLastProcessGroup();
599
- if (group) {
600
- markProcessGroupComplete(group, heading);
601
- }
602
-
603
- return drawStandaloneMessage(id, heading, content, {
604
- position: "left",
605
- forceNewGroup: true,
606
- containerClasses: ["ai-container"],
607
- mainClass: "message-agent-response",
608
- messageClasses: ["message-ai"],
930
+ let container = null;
931
+
932
+ if (group)
933
+ container = ensureChild(
934
+ group,
935
+ ".process-group-response",
936
+ "div",
937
+ "process-group-response",
938
+ );
939
+ else container = getOrCreateMessageContainer(id, "left");
940
+
941
+ const messageDiv = _drawMessage({
942
+ messageContainer: container,
943
+ heading: null,
944
+ content,
945
+ kvps: null,
946
+ messageClasses: [],
947
+ contentClasses: [],
948
markdown: true,
610
- latex: true
949
+ latex: true,
950
+ mainClass: "message-agent-response",
951
});
612
-}
952
953
+ // const copyText = copyContent ?? content ?? "";
954
+ // const speakText = speakContent ?? copyText;
955
+ // addActionButtonsToElement(messageDiv, {
956
+ // copyContent: copyText,
957
+ // speakContent: speakText,
958
+ // });
959
+
960
+ if (group) updateProcessGroupHeader(group);
961
+
962
+ return container;
963
+}
964
615
-export function drawMessageUser({ id, heading, content, kvps = null, ...additional }) {
616
- const messageContainer = getOrCreateMessageContainer(id, "right", ["user-container"], true);
965
+export function drawMessageUser({
966
+ id,
967
+ heading,
968
+ content,
969
+ kvps = null,
970
+ ...additional
971
+}) {
972
+ const messageContainer = getOrCreateMessageContainer(
973
+ id,
974
+ "right",
975
+ ["user-container"],
976
+ true,
977
+ );
978
979
// Find existing message div or create new one
980
let messageDiv = messageContainer.querySelector(".message");
@@ -653,7 +1014,7 @@ export function drawMessageUser({ id, heading, content, kvps = null, ...addition
1014
messageDiv.appendChild(attachmentsContainer);
1015
}
1016
// Important: Clear existing attachments to re-render, preventing duplicates on update
656
- attachmentsContainer.innerHTML = "";
1017
+ attachmentsContainer.innerHTML = "";
1018
1019
kvps.attachments.forEach((attachment) => {
1020
const attachmentDiv = document.createElement("div");
@@ -717,138 +1078,220 @@ export function drawMessageUser({ id, heading, content, kvps = null, ...addition
1078
}
1079
1080
// Add action buttons below text and attachments (hover for pointer, always for touch - via CSS)
720
- addActionButtonsToElement(messageDiv, { copyContent: content || "", speakContent: content || "" });
1081
+ addActionButtonsToElement(messageDiv, {
1082
+ copyContent: content || "",
1083
+ speakContent: content || "",
1084
+ });
1085
}
1086
723
-export function drawMessageTool({ id, type, heading, content, kvps = null, timestamp = null, agentno = 0, ...additional }) {
724
- const toolName = kvps?.tool_name || null;
725
- const title = getStepTitle(heading, kvps, type);
726
- const statusCode = getStatusCode(type, toolName);
727
- const statusClass = getStatusClass(type, toolName);
1087
+export function drawMessageTool({
1088
+ id,
1089
+ type,
1090
+ heading,
1091
+ content,
1092
+ kvps = null,
1093
+ timestamp = null,
1094
+ agentno = 0,
1095
+ ...additional
1096
+}) {
1097
+ const title = cleanStepTitle(heading);
1098
+ let displayKvps = { ...kvps };
1099
1100
return drawProcessStep({
1101
id,
1102
title,
732
- statusClass,
733
- statusCode,
734
- kvps,
735
- type,
736
- heading,
1103
+ code: "USE",
1104
+ codeClass: "status-tool",
1105
+ classes: null,
1106
+ kvps: displayKvps,
1107
content,
738
- timestamp,
739
- agentno,
740
- toolName
1108
+ // contentClasses: [],
1109
+ copyText: content,
1110
+ speakText: content,
1111
+ log: arguments[0],
1112
});
1113
}
1114
744
-export function drawMessageCodeExe({ id, type, heading, content, kvps = null, timestamp = null, agentno = 0, ...additional }) {
745
- const title = getStepTitle(heading, kvps, type);
746
- const statusCode = getStatusCode(type);
747
- const statusClass = getStatusClass(type);
1115
+export function drawMessageCodeExe({
1116
+ id,
1117
+ type,
1118
+ heading,
1119
+ content,
1120
+ kvps = null,
1121
+ timestamp = null,
1122
+ agentno = 0,
1123
+ ...additional
1124
+}) {
1125
+ let title = "Code Execution";
1126
+ // show command at the start and end
1127
+ if (
1128
+ type === "code_exe" &&
1129
+ kvps?.code &&
1130
+ /done_all|code_execution_tool/.test(heading || "")
1131
+ ) {
1132
+ const s = kvps.session ?? kvps.Session;
1133
+ title = `${s != null ? `[${s}] ` : ""}${kvps.runtime || "bash"}> ${kvps.code.trim()}`;
1134
+ } else {
1135
+ // during execution show the original heading (current step)
1136
+ title = cleanStepTitle(heading);
1137
+ }
1138
749
- return drawProcessStep({
1139
+ // KVPS to show
1140
+ const displayKvps = {};
1141
+ if (kvps?.runtime) displayKvps.runtime = kvps.runtime;
1142
+ if (kvps?.session) displayKvps.session = kvps.session;
1143
+
1144
+ // render the standard step
1145
+ const stepData = drawProcessStep({
1146
id,
1147
title,
752
- statusClass,
753
- statusCode,
754
- kvps,
755
- type,
756
- heading,
1148
+ code: "EXE",
1149
+ codeClass: "status-exe",
1150
+ classes: null,
1151
+ kvps: displayKvps,
1152
content,
758
- timestamp,
759
- agentno
1153
+ contentClasses: ["terminal-output"],
1154
+ copyText: content,
1155
+ speakText: null,
1156
+ log: arguments[0],
1157
});
1158
}
1159
763
-export function drawMessageBrowser({ id, type, heading, content, kvps = null, timestamp = null, agentno = 0, ...additional }) {
764
- const title = getStepTitle(heading, kvps, type);
765
- const statusCode = getStatusCode(type);
766
- const statusClass = getStatusClass(type);
767
-
768
- return drawProcessStep({
769
- id,
770
- title,
771
- statusClass,
772
- statusCode,
773
- kvps,
774
- type,
775
- heading,
1160
+export function drawMessageBrowser({
1161
+ id,
1162
+ type,
1163
+ heading,
1164
+ content,
1165
+ kvps = null,
1166
+ timestamp = null,
1167
+ agentno = 0,
1168
+ ...additional
1169
+}) {
1170
+ const title = cleanStepTitle(heading);
1171
+ let displayKvps = { ...kvps };
1172
+
1173
+ return drawProcessStep({
1174
+ id,
1175
+ title,
1176
+ code: "HDL",
1177
+ codeClass: "status-hdl",
1178
+ classes: null,
1179
+ kvps: displayKvps,
1180
content,
777
- timestamp,
778
- agentno
1181
+ // contentClasses: [],
1182
+ copyText: content,
1183
+ speakText: content,
1184
+ log: arguments[0],
1185
});
1186
}
1187
782
-export function drawMessageMcp({ id, type, heading, content, kvps = null, timestamp = null, agentno = 0, ...additional }) {
783
- const toolName = kvps?.tool_name || null;
784
- const title = getStepTitle(heading, kvps, type);
785
- const statusCode = getStatusCode(type, toolName);
786
- const statusClass = getStatusClass(type, toolName);
1188
+export function drawMessageMcp({
1189
+ id,
1190
+ type,
1191
+ heading,
1192
+ content,
1193
+ kvps = null,
1194
+ timestamp = null,
1195
+ agentno = 0,
1196
+ ...additional
1197
+}) {
1198
+ const title = cleanStepTitle(heading);
1199
+ let displayKvps = { ...kvps };
1200
1201
return drawProcessStep({
1202
id,
1203
title,
791
- statusClass,
792
- statusCode,
793
- kvps,
794
- type,
795
- heading,
1204
+ code: "MCP",
1205
+ codeClass: "status-mcp",
1206
+ classes: null,
1207
+ kvps: displayKvps,
1208
content,
797
- timestamp,
798
- agentno,
799
- toolName
1209
+ // contentClasses: [],
1210
+ copyText: content,
1211
+ speakText: content,
1212
+ log: arguments[0],
1213
});
1214
}
1215
803
-export function drawMessageSubagent({ id, type, heading, content, kvps = null, timestamp = null, agentno = 0, ...additional }) {
804
- const title = getStepTitle(heading, kvps, type);
805
- const statusCode = getStatusCode(type);
806
- const statusClass = getStatusClass(type);
1216
+export function drawMessageSubagent({
1217
+ id,
1218
+ type,
1219
+ heading,
1220
+ content,
1221
+ kvps = null,
1222
+ timestamp = null,
1223
+ agentno = 0,
1224
+ ...additional
1225
+}) {
1226
+ const title = cleanStepTitle(heading);
1227
+ let displayKvps = { ...kvps };
1228
1229
return drawProcessStep({
1230
id,
1231
title,
811
- statusClass,
812
- statusCode,
813
- kvps,
814
- type,
815
- heading,
1232
+ code: "SUB",
1233
+ codeClass: "status-sub",
1234
+ classes: null,
1235
+ kvps: displayKvps,
1236
content,
817
- timestamp,
818
- agentno
1237
+ // contentClasses: [],
1238
+ copyText: content,
1239
+ speakText: content,
1240
+ log: arguments[0],
1241
});
1242
}
1243
822
-
823
-export function drawMessageInfo({ id, heading, content, kvps = null, ...additional }) {
1244
+export function drawMessageInfo({
1245
+ id,
1246
+ heading,
1247
+ content,
1248
+ kvps = null,
1249
+ ...additional
1250
+}) {
1251
return drawStandaloneMessage(id, heading, content, {
1252
position: "mid",
1253
containerClasses: ["ai-container", "center-container"],
1254
mainClass: "message-info",
828
- kvps
1255
+ kvps,
1256
});
1257
}
1258
832
-export function drawMessageUtil({ id, type, heading, content, kvps = null, timestamp = null, agentno = 0, ...additional }) {
833
- const title = getStepTitle(heading, kvps, type);
834
- const statusCode = getStatusCode(type);
835
- const statusClass = getStatusClass(type);
1259
+export function drawMessageUtil({
1260
+ id,
1261
+ type,
1262
+ heading,
1263
+ content,
1264
+ kvps = null,
1265
+ timestamp = null,
1266
+ agentno = 0,
1267
+ ...additional
1268
+}) {
1269
+ const title = cleanStepTitle(heading);
1270
1271
return drawProcessStep({
1272
id,
1273
title,
840
- statusClass,
841
- statusCode,
1274
+ code: "UTL",
1275
+ codeClass: "status-utl",
1276
+ classes: ["message-util"],
1277
kvps,
843
- type,
844
- heading,
845
- content,
846
- timestamp,
847
- agentno
1278
+ copyText: null,
1279
+ speakText: null,
1280
+ log: arguments[0],
1281
+ allowCompletedGroup: true,
1282
});
1283
}
1284
851
-export function drawMessageHint({ id, type, heading, content, kvps = null, timestamp = null, agentno = 0, ...additional }) {
1285
+export function drawMessageHint({
1286
+ id,
1287
+ type,
1288
+ heading,
1289
+ content,
1290
+ kvps = null,
1291
+ timestamp = null,
1292
+ agentno = 0,
1293
+ ...additional
1294
+}) {
1295
const title = getStepTitle(heading, kvps, type);
1296
const statusCode = getStatusCode(type);
1297
const statusClass = getStatusClass(type);
@@ -863,167 +1306,227 @@ export function drawMessageHint({ id, type, heading, content, kvps = null, times
1306
heading,
1307
content,
1308
timestamp,
866
- agentno
1309
+ agentno,
1310
});
1311
}
1312
870
-export function drawMessageProgress({ id, type, heading, content, kvps = null, timestamp = null, agentno = 0, ...additional }) {
871
- const title = getStepTitle(heading, kvps, type);
872
- const statusCode = getStatusCode(type);
873
- const statusClass = getStatusClass(type);
1313
+export function drawMessageProgress({
1314
+ id,
1315
+ type,
1316
+ heading,
1317
+ content,
1318
+ kvps = null,
1319
+ timestamp = null,
1320
+ agentno = 0,
1321
+ ...additional
1322
+}) {
1323
+ const title = cleanStepTitle(heading);
1324
+ let displayKvps = { ...kvps };
1325
1326
return drawProcessStep({
1327
id,
1328
title,
878
- statusClass,
879
- statusCode,
880
- kvps,
881
- type,
882
- heading,
1329
+ code: "HDL",
1330
+ codeClass: "status-hdl",
1331
+ classes: null,
1332
+ kvps: displayKvps,
1333
content,
884
- timestamp,
885
- agentno
1334
+ // contentClasses: [],
1335
+ // copyText: kvps?.thoughts,
1336
+ // speakText: kvps?.thoughts,
1337
+ log: arguments[0],
1338
});
1339
}
1340
889
-export function drawMessageWarning({ id, heading, content, kvps = null, ...additional }) {
1341
+export function drawMessageWarning({
1342
+ id,
1343
+ heading,
1344
+ content,
1345
+ kvps = null,
1346
+ ...additional
1347
+}) {
1348
+ const title = cleanStepTitle(heading);
1349
+ let displayKvps = { ...kvps };
1350
+
1351
+ //TODO: if process group is running, append there instead
1352
+ // return drawProcessStep({
1353
+ // id,
1354
+ // title,
1355
+ // code: "WRN",
1356
+ // codeClass: "status-wrn",
1357
+ // classes: null,
1358
+ // kvps: displayKvps,
1359
+ // content,
1360
+ // // contentClasses: [],
1361
+ // copyText: content,
1362
+ // speakText: content,
1363
+ // log: arguments[0],
1364
+ // });
1365
return drawStandaloneMessage(id, heading, content, {
1366
position: "mid",
1367
containerClasses: ["ai-container", "center-container"],
1368
mainClass: "message-warning",
894
- kvps
1369
+ kvps,
1370
});
1371
}
1372
898
-export function drawMessageError({ id, heading, content, kvps = null, ...additional }) {
899
- const messageContainer = getOrCreateMessageContainer(id, "mid", ["ai-container", "center-container"]);
900
-
901
- // Create or get the message div
902
- let messageDiv = messageContainer.querySelector(".message");
903
- if (!messageDiv) {
904
- messageDiv = document.createElement("div");
905
- messageDiv.classList.add("message", "message-error-group");
906
- messageContainer.appendChild(messageDiv);
907
- }
908
-
909
- // Check if error group already exists
910
- let errorGroup = messageDiv.querySelector(".error-group");
911
- if (!errorGroup) {
912
- errorGroup = document.createElement("div");
913
- errorGroup.classList.add("error-group");
914
- errorGroup.setAttribute("data-error-id", id);
915
-
916
- // Create header (clickable for expand/collapse)
917
- const header = document.createElement("div");
918
- header.classList.add("error-group-header");
919
-
920
- // Expand icon (triangle)
921
- const expandIcon = document.createElement("span");
922
- expandIcon.classList.add("expand-icon");
923
- header.appendChild(expandIcon);
924
-
925
- // Status badge (before title)
926
- const badge = document.createElement("span");
927
- badge.classList.add("status-badge", "status-err");
928
- badge.textContent = "ERR";
929
- header.appendChild(badge);
930
-
931
- // Title
932
- const title = document.createElement("span");
933
- title.classList.add("error-title");
934
- title.textContent = "Error";
935
- header.appendChild(title);
936
-
937
- // Subtitle (short error description)
938
- const subtitle = document.createElement("span");
939
- subtitle.classList.add("error-subtitle");
940
- header.appendChild(subtitle);
941
-
942
- // Click handler for expand/collapse
943
- header.addEventListener("click", () => {
944
- errorGroup.classList.toggle("expanded");
945
- });
946
-
947
- errorGroup.appendChild(header);
948
-
949
- // Create content container (collapsible)
950
- const contentWrapper = document.createElement("div");
951
- contentWrapper.classList.add("error-group-content");
952
-
953
- const contentInner = document.createElement("div");
954
- contentInner.classList.add("error-content-inner");
955
- contentWrapper.appendChild(contentInner);
956
-
957
- errorGroup.appendChild(contentWrapper);
958
- messageDiv.appendChild(errorGroup);
959
-
960
- // Check detail mode and expand if needed
961
- const detailMode = preferencesStore.detailMode || "current";
962
- if (detailMode === "current" || detailMode === "expanded") {
963
- errorGroup.classList.add("expanded");
964
- }
965
- }
966
-
967
- // Update subtitle with short error description
968
- const subtitle = errorGroup.querySelector(".error-subtitle");
969
- if (subtitle) {
970
- // Extract short description from heading or content
971
- let shortDesc = "";
972
- // Skip if heading is just "Error" (redundant with title)
973
- if (heading && heading.trim() && heading.trim().toLowerCase() !== "error") {
974
- shortDesc = heading.trim();
975
- }
976
- // If no useful heading, try to extract from content
977
- if (!shortDesc && content && content.trim()) {
978
- const lines = content.trim().split("\n");
979
- // Look for the error line (usually last meaningful line or one matching ErrorType: pattern)
980
- for (let i = lines.length - 1; i >= 0; i--) {
981
- const line = lines[i].trim();
982
- if (line && /^[\w\.]+Error[:\s]/.test(line)) {
983
- shortDesc = line;
984
- break;
985
- }
986
- }
987
- // Fallback to first non-empty line if no error pattern found
988
- if (!shortDesc) {
989
- for (const line of lines) {
990
- if (line.trim() && !line.startsWith("Traceback")) {
991
- shortDesc = line.trim();
992
- break;
993
- }
994
- }
995
- }
996
- }
997
- // Truncate if too long
998
- if (shortDesc.length > 100) {
999
- shortDesc = shortDesc.substring(0, 97) + "...";
1000
- }
1001
- subtitle.textContent = shortDesc;
1002
- subtitle.title = shortDesc; // Full text on hover
1003
- }
1004
-
1005
- // Update content (full callstack)
1006
- const contentInner = errorGroup.querySelector(".error-content-inner");
1007
- if (contentInner && content) {
1008
- contentInner.innerHTML = "";
1009
-
1010
- // Create pre element for callstack/content
1011
- const pre = document.createElement("pre");
1012
- pre.classList.add("error-callstack");
1013
- pre.textContent = content;
1014
- contentInner.appendChild(pre);
1015
-
1016
- // Add action buttons for copy functionality
1017
- addActionButtonsToElement(contentInner, { copyContent: content, speakContent: content });
1018
- }
1019
-
1020
- messageContainer.classList.add("center-container");
1373
+export function drawMessageError({
1374
+ id,
1375
+ heading,
1376
+ content,
1377
+ kvps = null,
1378
+ ...additional
1379
+}) {
1380
+ return drawStandaloneMessage(id, heading, content, {
1381
+ position: "mid",
1382
+ containerClasses: ["ai-container", "center-container"],
1383
+ mainClass: "message-error",
1384
+ kvps,
1385
+ });
1386
}
1387
1388
+// export function drawMessageError({
1389
+// id,
1390
+// heading,
1391
+// content,
1392
+// kvps = null,
1393
+// ...additional
1394
+// }) {
1395
+// const messageContainer = getOrCreateMessageContainer(id, "mid", [
1396
+// "ai-container",
1397
+// "center-container",
1398
+// ]);
1399
+
1400
+// // Create or get the message div
1401
+// let messageDiv = messageContainer.querySelector(".message");
1402
+// if (!messageDiv) {
1403
+// messageDiv = document.createElement("div");
1404
+// messageDiv.classList.add("message", "message-error-group");
1405
+// messageContainer.appendChild(messageDiv);
1406
+// }
1407
+
1408
+// // Check if error group already exists
1409
+// let errorGroup = messageDiv.querySelector(".error-group");
1410
+// if (!errorGroup) {
1411
+// errorGroup = document.createElement("div");
1412
+// errorGroup.classList.add("error-group");
1413
+// errorGroup.setAttribute("data-error-id", id);
1414
+
1415
+// // Create header (clickable for expand/collapse)
1416
+// const header = document.createElement("div");
1417
+// header.classList.add("error-group-header");
1418
+
1419
+// // Expand icon (triangle)
1420
+// const expandIcon = document.createElement("span");
1421
+// expandIcon.classList.add("expand-icon");
1422
+// header.appendChild(expandIcon);
1423
+
1424
+// // Status badge (before title)
1425
+// const badge = document.createElement("span");
1426
+// badge.classList.add("step-badge", "status-err");
1427
+// badge.textContent = "ERR";
1428
+// header.appendChild(badge);
1429
+
1430
+// // Title
1431
+// const title = document.createElement("span");
1432
+// title.classList.add("error-title");
1433
+// title.textContent = "Error";
1434
+// header.appendChild(title);
1435
+
1436
+// // Subtitle (short error description)
1437
+// const subtitle = document.createElement("span");
1438
+// subtitle.classList.add("error-subtitle");
1439
+// header.appendChild(subtitle);
1440
+
1441
+// // Click handler for expand/collapse
1442
+// header.addEventListener("click", () => {
1443
+// errorGroup.classList.toggle("expanded");
1444
+// });
1445
+
1446
+// errorGroup.appendChild(header);
1447
+
1448
+// // Create content container (collapsible)
1449
+// const contentWrapper = document.createElement("div");
1450
+// contentWrapper.classList.add("error-group-content");
1451
+
1452
+// const contentInner = document.createElement("div");
1453
+// contentInner.classList.add("error-content-inner");
1454
+// contentWrapper.appendChild(contentInner);
1455
+
1456
+// errorGroup.appendChild(contentWrapper);
1457
+// messageDiv.appendChild(errorGroup);
1458
+
1459
+// // Check detail mode and expand if needed
1460
+// const detailMode = preferencesStore.detailMode || "current";
1461
+// if (detailMode === "current" || detailMode === "expanded") {
1462
+// errorGroup.classList.add("expanded");
1463
+// }
1464
+// }
1465
+
1466
+// // Update subtitle with short error description
1467
+// const subtitle = errorGroup.querySelector(".error-subtitle");
1468
+// if (subtitle) {
1469
+// // Extract short description from heading or content
1470
+// let shortDesc = "";
1471
+// // Skip if heading is just "Error" (redundant with title)
1472
+// if (heading && heading.trim() && heading.trim().toLowerCase() !== "error") {
1473
+// shortDesc = heading.trim();
1474
+// }
1475
+// // If no useful heading, try to extract from content
1476
+// if (!shortDesc && content && content.trim()) {
1477
+// const lines = content.trim().split("\n");
1478
+// // Look for the error line (usually last meaningful line or one matching ErrorType: pattern)
1479
+// for (let i = lines.length - 1; i >= 0; i--) {
1480
+// const line = lines[i].trim();
1481
+// if (line && /^[\w\.]+Error[:\s]/.test(line)) {
1482
+// shortDesc = line;
1483
+// break;
1484
+// }
1485
+// }
1486
+// // Fallback to first non-empty line if no error pattern found
1487
+// if (!shortDesc) {
1488
+// for (const line of lines) {
1489
+// if (line.trim() && !line.startsWith("Traceback")) {
1490
+// shortDesc = line.trim();
1491
+// break;
1492
+// }
1493
+// }
1494
+// }
1495
+// }
1496
+// // Truncate if too long
1497
+// if (shortDesc.length > 100) {
1498
+// shortDesc = shortDesc.substring(0, 97) + "...";
1499
+// }
1500
+// subtitle.textContent = shortDesc;
1501
+// subtitle.title = shortDesc; // Full text on hover
1502
+// }
1503
+
1504
+// // Update content (full callstack)
1505
+// const contentInner = errorGroup.querySelector(".error-content-inner");
1506
+// if (contentInner && content) {
1507
+// contentInner.innerHTML = "";
1508
+
1509
+// // Create pre element for callstack/content
1510
+// const pre = document.createElement("pre");
1511
+// pre.classList.add("error-callstack");
1512
+// pre.textContent = content;
1513
+// contentInner.appendChild(pre);
1514
+
1515
+// // Add action buttons for copy functionality
1516
+// addActionButtonsToElement(contentInner, {
1517
+// copyContent: content,
1518
+// speakContent: content,
1519
+// });
1520
+// }
1521
+
1522
+// messageContainer.classList.add("center-container");
1523
+// }
1524
+
1525
function drawKvpsIncremental(container, kvps, latex) {
1526
+ // existing KVPS table
1527
+ let table = container.querySelector(".msg-kvps");
1528
if (kvps) {
1025
- // Find existing table or create new one
1026
- let table = container.querySelector(".msg-kvps");
1529
+ // create table if not found
1530
if (!table) {
1531
table = document.createElement("table");
1532
table.classList.add("msg-kvps");
@@ -1033,7 +1536,9 @@ function drawKvpsIncremental(container, kvps, latex) {
1536
// Get all current rows for comparison
1537
let existingRows = table.querySelectorAll(".kvps-row");
1538
// Filter out reasoning
1036
- const kvpEntries = Object.entries(kvps).filter(([key]) => key !== "reasoning");
1539
+ const kvpEntries = Object.entries(kvps).filter(
1540
+ ([key]) => key !== "reasoning",
1541
+ );
1542
1543
// Update or create rows as needed
1544
kvpEntries.forEach(([key, value], index) => {
@@ -1065,33 +1570,26 @@ function drawKvpsIncremental(container, kvps, latex) {
1570
let td = row.cells[1];
1571
if (!td) {
1572
td = row.insertCell(1);
1068
- }
1069
-
1070
- let tdiv = td.querySelector(".kvps-val");
1071
- if (!tdiv) {
1072
- tdiv = document.createElement("div");
1073
- tdiv.classList.add("kvps-val");
1074
- td.appendChild(tdiv);
1573
+ td.classList.add("kvps-val");
1574
}
1575
1576
// reapply scroll position or autoscroll
1078
- const scroller = new Scroller(tdiv);
1577
+ // no inner scrolling for kvps anymore
1578
+ // const scroller = new Scroller(td);
1579
1580
// Clear and rebuild content (for now - could be optimized further)
1081
- tdiv.innerHTML = "";
1082
-
1083
- // addActionButtonsToElement(tdiv);
1581
+ td.innerHTML = "";
1582
1583
if (Array.isArray(value)) {
1584
for (const item of value) {
1087
- addValue(item, tdiv);
1585
+ addValue(item, td);
1586
}
1587
} else {
1090
- addValue(value, tdiv);
1588
+ addValue(value, td);
1589
}
1590
1591
// reapply scroll position or autoscroll
1094
- scroller.reApplyScroll();
1592
+ // scroller.reApplyScroll();
1593
});
1594
1595
// Remove extra rows if we have fewer kvps now
@@ -1117,18 +1615,9 @@ function drawKvpsIncremental(container, kvps, latex) {
1615
imageViewerStore.open(imgElement.src, { refreshInterval: 1000 });
1616
});
1617
} else {
1120
- // const pre = document.createElement("pre");
1618
const span = document.createElement("span");
1619
span.innerHTML = convertHTML(value);
1123
- // pre.appendChild(span);
1124
- // tdiv.appendChild(pre);
1125
- tdiv.appendChild(span)
1126
-
1127
- // Add action buttons to the row
1128
- // const row = tdiv.closest(".kvps-row");
1129
- // if (row) {
1130
- // addActionButtonsToElement(pre);
1131
- // }
1620
+ tdiv.appendChild(span);
1621
1622
// KaTeX rendering for markdown
1623
if (latex) {
@@ -1142,11 +1631,10 @@ function drawKvpsIncremental(container, kvps, latex) {
1631
}
1632
} else {
1633
// Remove table if kvps is null/empty
1145
- const existingTable = container.querySelector(".msg-kvps");
1146
- if (existingTable) {
1147
- existingTable.remove();
1148
- }
1634
+ if (table) existingTable.remove();
1635
+ return null;
1636
}
1637
+ return table;
1638
}
1639
1640
function convertToTitleCase(str) {
@@ -1162,17 +1650,22 @@ function convertToTitleCase(str) {
1650
* Clean text value by removing standalone bracket lines and trimming
1651
* Handles both strings and arrays (filters out bracket-only items)
1652
*/
1165
-function cleanTextValue(value) {
1166
- if (Array.isArray(value)) {
1167
- return value
1168
- .filter(item => item && String(item).trim() && !/^[\[\]]$/.test(String(item).trim()))
1169
- .join("\n");
1170
- }
1171
- if (typeof value === "object" && value !== null) {
1172
- return JSON.stringify(value, null, 2);
1173
- }
1174
- return String(value).replace(/^\s*[\[\]]\s*$/gm, "").trim();
1175
-}
1653
+// function cleanTextValue(value) {
1654
+// if (Array.isArray(value)) {
1655
+// return value
1656
+// .filter(
1657
+// (item) =>
1658
+// item && String(item).trim() && !/^[\[\]]$/.test(String(item).trim()),
1659
+// )
1660
+// .join("\n");
1661
+// }
1662
+// if (typeof value === "object" && value !== null) {
1663
+// return JSON.stringify(value, null, 2);
1664
+// }
1665
+// return String(value)
1666
+// .replace(/^\s*[\[\]]\s*$/gm, "")
1667
+// .trim();
1668
+// }
1669
1670
function convertImageTags(content) {
1671
// Regular expression to match <image> tags and extract base64 content
@@ -1183,7 +1676,7 @@ function convertImageTags(content) {
1676
imageTagRegex,
1677
(match, base64Content) => {
1678
return `<img src="data:image/jpeg;base64,${base64Content}" alt="Image Attachment" style="max-width: 250px !important;"/>`;
1186
- }
1679
+ },
1680
);
1681
1682
return updatedContent;
@@ -1209,7 +1702,7 @@ function convertFilePaths(str) {
1702
export function convertIcons(str) {
1703
return str.replace(
1704
/icon:\/\/([a-zA-Z0-9_]+)/g,
1212
- '<span class="icon material-symbols-outlined">$1</span>'
1705
+ '<span class="icon material-symbols-outlined">$1</span>',
1706
);
1707
}
1708
@@ -1243,7 +1736,7 @@ function convertPathsToLinks(str) {
1736
const suffix = `(?<!\\.)`;
1737
const pathRegex = new RegExp(
1738
`(?<=${prefix})\\/${folder}*${file}${suffix}`,
1246
- "g"
1739
+ "g",
1740
);
1741
1742
// skip paths inside html tags, like <img src="/path/to/image">
@@ -1283,7 +1776,10 @@ function adjustMarkdownRender(element) {
1776
link.href = img.src;
1777
img.parentNode.insertBefore(link, img);
1778
link.appendChild(img);
1286
- link.onclick = (e) => (e.preventDefault(), imageViewerStore.open(img.src, { name: img.alt || "Image" }));
1779
+ link.onclick = (e) => (
1780
+ e.preventDefault(),
1781
+ imageViewerStore.open(img.src, { name: img.alt || "Image" })
1782
+ );
1783
});
1784
}
1785
@@ -1306,14 +1802,6 @@ class Scroller {
1802
}
1803
}
1804
1309
-// ============================================
1310
-// Process Group Embedding Functions
1311
-// ============================================
1312
-
1313
-// ============================================
1314
-// Process Group Functions
1315
-// ============================================
1316
-
1805
/**
1806
* Create a new collapsible process group
1807
*/
@@ -1323,20 +1811,20 @@ function createProcessGroup(id) {
1811
group.id = groupId;
1812
group.classList.add("process-group");
1813
group.setAttribute("data-group-id", groupId);
1326
-
1814
+
1815
// Determine initial expansion state from current detail mode
1816
const initiallyExpanded = preferencesStore.detailMode !== "collapsed";
1817
if (initiallyExpanded) {
1330
- group.classList.add('expanded');
1818
+ group.classList.add("expanded");
1819
}
1332
-
1820
+
1821
// Create header
1822
const header = document.createElement("div");
1823
header.classList.add("process-group-header");
1824
header.innerHTML = `
1825
<span class="expand-icon"></span>
1826
<span class="group-title">Processing...</span>
1339
- <span class="status-badge status-gen group-status">GEN</span>
1827
+ <span class="step-badge GEN">GEN</span>
1828
<span class="group-metrics">
1829
<span class="metric-time" title="Start time"><span class="material-symbols-outlined">schedule</span><span class="metric-value">--:--</span></span>
1830
<span class="metric-steps" title="Steps"><span class="material-symbols-outlined">footprint</span><span class="metric-value">0</span></span>
@@ -1344,25 +1832,25 @@ function createProcessGroup(id) {
1832
<span class="metric-duration" title="Duration"><span class="material-symbols-outlined">timer</span><span class="metric-value">0s</span></span>
1833
</span>
1834
`;
1347
-
1835
+
1836
// Add click handler for expansion
1837
header.addEventListener("click", () => {
1838
group.classList.toggle("expanded");
1839
});
1352
-
1840
+
1841
group.appendChild(header);
1354
-
1842
+
1843
// Create content container
1844
const content = document.createElement("div");
1845
content.classList.add("process-group-content");
1358
-
1846
+
1847
// Create steps container
1848
const steps = document.createElement("div");
1849
steps.classList.add("process-steps");
1850
content.appendChild(steps);
1363
-
1851
+
1852
group.appendChild(content);
1365
-
1853
+
1854
return group;
1855
}
1856
@@ -1371,21 +1859,21 @@ function createProcessGroup(id) {
1859
*/
1860
function getNestedContainer(parentStep) {
1861
let nestedContainer = parentStep.querySelector(".process-nested-container");
1374
-
1862
+
1863
if (!nestedContainer) {
1864
// Create new container
1865
nestedContainer = document.createElement("div");
1866
nestedContainer.classList.add("process-nested-container");
1379
-
1867
+
1868
// Create inner wrapper for animation support
1869
const innerWrapper = document.createElement("div");
1870
innerWrapper.classList.add("process-nested-inner");
1871
nestedContainer.appendChild(innerWrapper);
1384
-
1872
+
1873
parentStep.appendChild(nestedContainer);
1874
parentStep.classList.add("has-nested-steps");
1875
}
1388
-
1876
+
1877
// Return the inner wrapper for appending steps
1878
const innerWrapper = nestedContainer.querySelector(".process-nested-inner");
1879
return innerWrapper || nestedContainer; // Fallback to container if wrapper missing
@@ -1400,7 +1888,7 @@ function scheduleStepCollapse(stepElement, delayMs) {
1888
if (stepElement.hasAttribute("data-collapse-timeout-id")) {
1889
return;
1890
}
1403
-
1891
+
1892
// Schedule the collapse
1893
const timeoutId = setTimeout(() => {
1894
stepElement.classList.remove("step-expanded");
@@ -1437,21 +1925,23 @@ function addStepCollapseInteractionHandlers(stepElement) {
1925
cancelStepCollapse(stepElement);
1926
}
1927
});
1440
-
1928
+
1929
// On leave, start a new timeout ONLY if user hasn't explicitly clicked
1930
// and only in "current" mode (in "expanded" mode, everything stays open)
1931
stepElement.addEventListener("mouseleave", () => {
1932
const detailMode = preferencesStore.detailMode;
1933
// Don't schedule collapse in "expanded" mode - user wants everything open
1934
if (detailMode === "expanded") return;
1447
-
1935
+
1936
// Don't restart timeout if user has explicitly interacted (clicked)
1449
- if (stepElement.classList.contains("step-expanded") &&
1450
- !stepElement.hasAttribute("data-user-pinned")) {
1937
+ if (
1938
+ stepElement.classList.contains("step-expanded") &&
1939
+ !stepElement.hasAttribute("data-user-pinned")
1940
+ ) {
1941
scheduleStepCollapse(stepElement, STEP_COLLAPSE_DELAY_MS);
1942
}
1943
});
1454
-
1944
+
1945
// On click anywhere on step, permanently cancel auto-collapse
1946
stepElement.addEventListener("click", () => {
1947
if (stepElement.classList.contains("step-expanded")) {
@@ -1466,353 +1956,51 @@ function addStepCollapseInteractionHandlers(stepElement) {
1956
* Find parent delegation step for nested agents (DOM-first, reverse scan).
1957
*/
1958
function findParentDelegationStep(group, agentno) {
1469
- if (!group || agentno <= 0) return null;
1959
+ if (!group || !agentno || agentno <= 0) return null;
1960
const steps = group.querySelectorAll(".process-step");
1961
for (let i = steps.length - 1; i >= 0; i -= 1) {
1962
const step = steps[i];
1963
const stepAgent = Number(step.getAttribute("data-agent-number"));
1474
- if (stepAgent === agentno - 1 && step.getAttribute("data-tool-name") === "call_subordinate") {
1964
+ if (
1965
+ stepAgent === agentno - 1 &&
1966
+ step.getAttribute("data-log-type") === "tool" // map to the last tool call of superior agent
1967
+ ) {
1968
return step;
1969
}
1970
}
1971
return null;
1972
}
1973
1481
-/**
1482
- * Add a step to a process group
1483
- */
1484
-function addProcessStep(group, stepData, detailPayload, copyContent, speakContent, detailHandler) {
1485
- const {
1486
- id,
1487
- type,
1488
- title,
1489
- heading,
1490
- content,
1491
- kvps,
1492
- timestamp,
1493
- agentno,
1494
- toolName,
1495
- statusCode,
1496
- statusClass
1497
- } = stepData;
1498
-
1499
- const stepsContainer = group.querySelector(".process-steps");
1500
- const isGroupCompleted = group.classList.contains("process-group-completed");
1501
-
1502
- // Create step element
1503
- const step = document.createElement("div");
1504
- step.id = `process-step-${id}`;
1505
- step.classList.add("process-step");
1506
- step.setAttribute("data-type", type);
1507
- step.setAttribute("data-step-id", id);
1508
- step.setAttribute("data-agent-number", agentno);
1509
-
1510
- if (toolName) {
1511
- step.setAttribute("data-tool-name", toolName);
1512
- }
1513
-
1514
- // Store timestamp for duration calculation
1515
- if (timestamp) {
1516
- step.setAttribute("data-timestamp", timestamp);
1517
-
1518
- // Set group start time from first log item
1519
- if (!group.getAttribute("data-start-timestamp")) {
1520
- group.setAttribute("data-start-timestamp", timestamp);
1521
- // Update header time metric immediately
1522
- const timeMetricEl = group.querySelector(".metric-time .metric-value");
1523
- if (timeMetricEl) {
1524
- const date = new Date(parseFloat(timestamp) * 1000);
1525
- const hours = String(date.getHours()).padStart(2, "0");
1526
- const minutes = String(date.getMinutes()).padStart(2, "0");
1527
- timeMetricEl.textContent = `${hours}:${minutes}`;
1528
- }
1529
- }
1530
- }
1531
-
1532
- // // Add message-util class for utility/info types (controlled by showUtils preference)
1533
- // if (type === "util" || type === "info" || type === "hint") {
1534
- // step.classList.add("message-util");
1535
- // // Apply current preference state
1536
- // if (preferencesStore.showUtils) {
1537
- // step.classList.add("show-util");
1538
- // }
1539
- // }
1540
-
1541
- // Determine if this new step should be expanded
1542
- const detailMode = preferencesStore.detailMode;
1543
- const isActiveGroup = group.classList.contains("active");
1544
- let shouldExpand = false;
1545
-
1546
- if (detailMode === "expanded") {
1547
- shouldExpand = true;
1548
- } else if (detailMode === "current") {
1549
- // Only expand and schedule timeouts for the ACTIVE group (currently streaming)
1550
- // For non-active groups (historical data), render steps collapsed immediately
1551
- if (isActiveGroup && !isGroupCompleted) {
1552
- shouldExpand = true;
1553
-
1554
- // Schedule collapse for ALL previously expanded steps
1555
- const allExpandedSteps = stepsContainer.querySelectorAll(".process-step.step-expanded");
1556
- allExpandedSteps.forEach(expandedStep => {
1557
- // Don't schedule collapse for the newly added step (the current one)
1558
- if (expandedStep.id !== `process-step-${id}`) {
1559
- scheduleStepCollapse(expandedStep, STEP_COLLAPSE_DELAY_MS);
1560
- }
1561
- });
1562
- }
1563
- // Non-active groups: shouldExpand stays false → steps render collapsed
1564
- }
1565
- // In "collapsed" mode: shouldExpand stays false
1566
-
1567
- if (shouldExpand) {
1568
- step.classList.add("step-expanded");
1569
- }
1570
-
1571
- // Create step header
1572
- const stepHeader = document.createElement("div");
1573
- stepHeader.classList.add("process-step-header");
1574
-
1575
- const resolvedTitle = title || getStepTitle(heading, kvps, type);
1576
- const resolvedStatusCode = statusCode || getStatusCode(type, toolName);
1577
- const resolvedStatusClass = statusClass || getStatusClass(type, toolName);
1578
-
1579
- // Add status color class to step for cascading --step-accent to internal icons
1580
- step.classList.add(resolvedStatusClass);
1581
- step.setAttribute("data-status-code", resolvedStatusCode);
1582
- step.setAttribute("data-status-class", resolvedStatusClass);
1583
-
1584
- stepHeader.innerHTML = `
1585
- <span class="step-expand-icon"></span>
1586
- <span class="status-badge ${resolvedStatusClass}">${resolvedStatusCode}</span>
1587
- <span class="step-title">${escapeHTML(resolvedTitle)}</span>
1588
- `;
1589
-
1590
- // Add click handler for step expansion
1591
- stepHeader.addEventListener("click", (e) => {
1592
- e.stopPropagation();
1593
-
1594
- // Cancel any scheduled auto-collapse (user is manually toggling)
1595
- cancelStepCollapse(step);
1596
-
1597
- // Toggle step
1598
- step.classList.toggle("step-expanded");
1599
-
1600
- // If manually expanded, set pinned flag to prevent auto-collapse
1601
- // If collapsed, remove it
1602
- if (step.classList.contains("step-expanded")) {
1603
- step.setAttribute("data-user-pinned", "true");
1604
- } else {
1605
- step.removeAttribute("data-user-pinned");
1606
- }
1607
- });
1608
-
1609
- step.appendChild(stepHeader);
1610
-
1611
- // Create step detail container
1612
- const detail = document.createElement("div");
1613
- detail.classList.add("process-step-detail");
1614
-
1615
- const detailContent = document.createElement("div");
1616
- detailContent.classList.add("process-step-detail-content");
1617
-
1618
- // Add content to detail
1619
- renderStepDetailContent(detailContent, content, kvps, type);
1620
-
1621
- detail.appendChild(detailContent);
1622
-
1623
- // Add step action buttons (view details, copy, speak)
1624
- const stepActionBtns = document.createElement("div");
1625
- stepActionBtns.classList.add("step-detail-actions");
1626
- addActionButtonsToElement(stepActionBtns, {
1627
- detailPayload,
1628
- onViewDetails: detailHandler,
1629
- copyContent,
1630
- speakContent
1631
- });
1632
- detail.appendChild(stepActionBtns);
1633
-
1634
- step.appendChild(detail);
1635
-
1636
- // Determine where to append the step (main list or nested in parent)
1637
- let appendTarget = stepsContainer;
1638
-
1639
- // Check if this step belongs to a subordinate agent
1640
- const parentStep = findParentDelegationStep(group, agentno);
1641
- if (parentStep) {
1642
- appendTarget = getNestedContainer(parentStep);
1643
- step.classList.add("nested-step");
1644
- }
1645
-
1646
- // Clear shiny effect from all previous steps in this group
1647
- group.querySelectorAll(".process-step .step-title.shiny-text").forEach(el => {
1648
- el.classList.remove("shiny-text");
1649
- });
1650
-
1651
- appendTarget.appendChild(step);
1652
-
1653
- // Add interaction handlers to prevent fighting with user during auto-collapse
1654
- addStepCollapseInteractionHandlers(step);
1655
-
1656
- // Scroll terminal to bottom on initial render (including page refresh)
1657
- const initialTerminal = step.querySelector(".terminal-output");
1658
- if (initialTerminal) {
1659
- initialTerminal.scrollTop = initialTerminal.scrollHeight;
1660
- }
1661
-
1662
- // Update group header
1663
- updateProcessGroupHeader(group);
1664
-
1665
- // Apply shiny effect to the new step's title if group is still active
1666
- if (!isGroupCompleted) {
1667
- const titleEl = step.querySelector(".process-step-header .step-title");
1668
- if (titleEl) {
1669
- titleEl.classList.add("shiny-text");
1670
- }
1671
- }
1672
-
1673
- return step;
1674
-}
1675
-
1676
-/**
1677
- * Update an existing process step
1678
- */
1679
-function updateProcessStep(stepElement, stepData, detailPayload, copyContent, speakContent, detailHandler) {
1680
- const {
1681
- type,
1682
- title,
1683
- heading,
1684
- content,
1685
- kvps,
1686
- timestamp,
1687
- agentno,
1688
- toolName,
1689
- statusCode,
1690
- statusClass
1691
- } = stepData;
1692
-
1693
- const titleEl = stepElement.querySelector(".step-title");
1694
- if (titleEl) {
1695
- const resolvedTitle = title || getStepTitle(heading, kvps, type);
1696
- titleEl.textContent = resolvedTitle;
1697
- }
1698
-
1699
- if (timestamp && !stepElement.hasAttribute("data-timestamp")) {
1700
- stepElement.setAttribute("data-timestamp", timestamp);
1701
- }
1702
-
1703
- if (agentno !== undefined) {
1704
- stepElement.setAttribute("data-agent-number", agentno);
1705
- }
1706
-
1707
- const toolNameToUse = resolveToolName(type, kvps, stepElement) || toolName;
1708
- if (toolNameToUse) {
1709
- stepElement.setAttribute("data-tool-name", toolNameToUse);
1710
- }
1711
-
1712
- const resolvedStatusCode = statusCode || getStatusCode(type, toolNameToUse);
1713
- const resolvedStatusClass = statusClass || getStatusClass(type, toolNameToUse);
1714
- const badge = stepElement.querySelector(".status-badge");
1715
- if (badge) {
1716
- updateBadgeText(badge, resolvedStatusCode);
1717
- badge.className = `status-badge ${resolvedStatusClass}`;
1718
- }
1719
-
1720
- const previousStatusClass = stepElement.getAttribute("data-status-class");
1721
- if (previousStatusClass) {
1722
- stepElement.classList.remove(previousStatusClass);
1723
- }
1724
- stepElement.classList.add(resolvedStatusClass);
1725
- stepElement.setAttribute("data-status-code", resolvedStatusCode);
1726
- stepElement.setAttribute("data-status-class", resolvedStatusClass);
1727
-
1728
- // Update detail content
1729
- const detailContent = stepElement.querySelector(".process-step-detail-content");
1730
- let skipFullRender = false;
1731
-
1732
- if (detailContent) {
1733
- // Capture scroll state before re-render (uses existing Scroller pattern)
1734
- const terminal = detailContent.querySelector(".terminal-output");
1735
- const scroller = terminal ? new Scroller(terminal) : null;
1736
-
1737
- // For browser, update image src incrementally to avoid flashing
1738
- if (type === "browser" && kvps?.screenshot) {
1739
- const existingImg = detailContent.querySelector(".screenshot-img");
1740
- const newSrc = kvps.screenshot.replace("img://", "/image_get?path=");
1741
- if (existingImg) {
1742
- // Only update if src actually changed
1743
- if (!existingImg.src.endsWith(newSrc.split("?path=")[1])) {
1744
- existingImg.src = newSrc;
1745
- }
1746
- // Skip full re-render to avoid flashing, but still update group header
1747
- skipFullRender = true;
1748
- }
1749
- }
1750
-
1751
- if (!skipFullRender) {
1752
- renderStepDetailContent(detailContent, content, kvps, type);
1753
-
1754
- // Re-apply scroll (stays at bottom if was at bottom)
1755
- const newTerminal = detailContent.querySelector(".terminal-output");
1756
- if (newTerminal && scroller?.wasAtBottom) {
1757
- newTerminal.scrollTop = newTerminal.scrollHeight;
1758
- }
1759
- }
1760
- }
1761
-
1762
- const detailData = detailPayload || buildDetailPayload({
1763
- ...stepData,
1764
- toolName: toolNameToUse,
1765
- statusCode: resolvedStatusCode,
1766
- statusClass: resolvedStatusClass
1767
- });
1768
-
1769
- const stepActions = stepElement.querySelector(".step-detail-actions") || document.createElement("div");
1770
- if (!stepActions.classList.contains("step-detail-actions")) {
1771
- stepActions.classList.add("step-detail-actions");
1772
- stepElement.querySelector(".process-step-detail")?.appendChild(stepActions);
1773
- }
1774
- addActionButtonsToElement(stepActions, {
1775
- detailPayload: detailData,
1776
- onViewDetails: detailHandler,
1777
- copyContent,
1778
- speakContent
1779
- });
1780
-
1781
- // Update parent group header
1782
- const group = stepElement.closest(".process-group");
1783
- if (group) {
1784
- updateProcessGroupHeader(group);
1785
- }
1786
-}
1787
-
1974
/**
1975
* Get a concise title for a process step
1976
*/
1977
function getStepTitle(heading, kvps, type) {
1978
// code_exe: show command when finished
1793
- const showCommand = type === "code_exe" && kvps?.code &&
1794
- /done_all|code_execution_tool/.test(heading || "");
1795
- if (showCommand) {
1796
- const s = kvps.session ?? kvps.Session;
1797
- return `${s != null ? `[${s}] ` : ""}${kvps.runtime || "bash"}> ${kvps.code.trim()}`;
1798
- }
1979
+ // const showCommand =
1980
+ // type === "code_exe" &&
1981
+ // kvps?.code &&
1982
+ // /done_all|code_execution_tool/.test(heading || "");
1983
+ // if (showCommand) {
1984
+ // const s = kvps.session ?? kvps.Session;
1985
+ // return `${s != null ? `[${s}] ` : ""}${kvps.runtime || "bash"}> ${kvps.code.trim()}`;
1986
+ // }
1987
1988
// Try to get a meaningful title from heading or kvps
1989
if (heading && heading.trim()) {
1990
return cleanStepTitle(heading, 100);
1991
}
1804
-
1992
+
1993
// For warnings/errors without heading, use content preview as title
1806
- if ((type === "warning" || type === "error")) {
1994
+ if (type === "warning" || type === "error") {
1995
// We'll show full content in detail, so just use type as title
1996
return type === "warning" ? "Warning" : "Error";
1997
}
1810
-
1998
+
1999
if (kvps) {
2000
// Try common fields for title
2001
if (kvps.tool_name) {
1814
- const headline = kvps.headline ? cleanStepTitle(kvps.headline, 60) : '';
1815
- return `${kvps.tool_name}${headline ? ': ' + headline : ''}`;
2002
+ const headline = kvps.headline ? cleanStepTitle(kvps.headline, 60) : "";
2003
+ return `${kvps.tool_name}${headline ? ": " + headline : ""}`;
2004
}
2005
if (kvps.headline) {
2006
return cleanStepTitle(kvps.headline, 100);
@@ -1824,9 +2012,11 @@ function getStepTitle(heading, kvps, type) {
2012
return truncateText(String(kvps.thoughts), 100);
2013
}
2014
}
1827
-
2015
+
2016
// Fallback: capitalize type (backend is source of truth)
1829
- return type ? type.charAt(0).toUpperCase() + type.slice(1).replace(/_/g, ' ') : 'Process';
2017
+ return type
2018
+ ? type.charAt(0).toUpperCase() + type.slice(1).replace(/_/g, " ")
2019
+ : "Process";
2020
}
2021
2022
/**
@@ -1842,282 +2032,259 @@ function extractIconFromKey(key) {
2032
* Clean step title by removing icon:// prefixes and status phrases
2033
* Preserves agent markers (A1:, A2:, etc.) so users can see which subordinate agent is executing
2034
*/
1845
-function cleanStepTitle(text, maxLength) {
2035
+function cleanStepTitle(text, maxLength = 100) {
2036
if (!text) return "";
2037
let cleaned = String(text);
1848
-
2038
+
2039
// Remove icon:// patterns (e.g., "icon://network_intelligence")
2040
cleaned = cleaned.replace(/icon:\/\/[a-zA-Z0-9_]+\s*/g, "");
1851
-
2041
+
2042
// Trim whitespace
2043
cleaned = cleaned.trim();
1854
-
2044
+
2045
return truncateText(cleaned, maxLength);
2046
}
2047
2048
/**
2049
* Render content for step detail panel
2050
*/
1861
-function renderStepDetailContent(container, content, kvps, type = null) {
1862
- container.innerHTML = "";
1863
-
1864
- drawKvpsIncremental(container, kvps)
1865
-
1866
- // Special handling for response type - show content as markdown (for subordinate responses)
1867
- if (type === "response" && content && content.trim()) {
1868
- const responseDiv = document.createElement("div");
1869
- responseDiv.classList.add("step-response-content");
1870
-
1871
- // Parse markdown
1872
- let processedContent = content;
1873
- processedContent = convertImageTags(processedContent);
1874
- processedContent = convertImgFilePaths(processedContent);
1875
- processedContent = convertFilePaths(processedContent);
1876
- processedContent = marked.parse(processedContent, { breaks: true });
1877
- processedContent = convertPathsToLinks(processedContent);
1878
- processedContent = addBlankTargetsToLinks(processedContent);
1879
-
1880
- responseDiv.innerHTML = processedContent;
1881
- container.appendChild(responseDiv);
1882
- return;
1883
- }
1884
-
1885
- // Special handling for warning/error types - always show content prominently
1886
- if ((type === "warning" || type === "error") && content && content.trim()) {
1887
- const warningDiv = document.createElement("div");
1888
- warningDiv.classList.add("step-warning-content");
1889
- warningDiv.textContent = content;
1890
- container.appendChild(warningDiv);
1891
- // Don't return - also show kvps if present
1892
- }
1893
-
1894
- // Special handling for code_exe type - render as terminal-style output
1895
- if (type === "code_exe" && kvps) {
1896
- const runtime = kvps.runtime || kvps.Runtime || "bash";
1897
- const code = kvps.code || kvps.Code || "";
1898
- const output = content || "";
1899
-
1900
- if (code || output) {
1901
- const terminalDiv = document.createElement("div");
1902
- terminalDiv.classList.add("step-terminal");
1903
-
1904
- // Show output if present (no truncation - CSS handles max-height)
1905
- if (output && output.trim()) {
1906
- const outputPre = document.createElement("pre");
1907
- outputPre.classList.add("terminal-output");
1908
- // Escape HTML first, then convert paths to clickable links
1909
- let processedOutput = escapeHTML(output);
1910
- processedOutput = convertPathsToLinks(processedOutput);
1911
- outputPre.innerHTML = processedOutput;
1912
- terminalDiv.appendChild(outputPre);
1913
- }
1914
-
1915
- container.appendChild(terminalDiv);
1916
- }
1917
-
1918
- // Still render thoughts if present (but not reasoning - that's native model thinking, not structured output)
1919
- if (kvps.thoughts || kvps.thinking) {
1920
- const thoughtKey = kvps.thoughts ? "thoughts" : "thinking";
1921
- const thoughtValue = kvps[thoughtKey];
1922
- renderThoughts(container, thoughtValue);
1923
- }
1924
-
1925
- return;
1926
- }
1927
-
1928
- // Add KVPs if present
1929
- if (kvps && Object.keys(kvps).length > 0) {
1930
- const kvpsDiv = document.createElement("div");
1931
- kvpsDiv.classList.add("step-kvps");
1932
-
1933
- for (const [key, value] of Object.entries(kvps)) {
1934
- // Skip internal/display keys
1935
- if (key === "finished" || key === "attachments") continue;
1936
-
1937
- // Skip code_exe specific keys that we handle specially above
1938
- if (type === "code_exe" && (key.toLowerCase() === "runtime" || key.toLowerCase() === "session" || key.toLowerCase() === "code")) {
1939
- continue;
1940
- }
1941
-
1942
- const lowerKey = key.toLowerCase();
1943
-
1944
- // Skip headline and tool_name - they're shown elsewhere
1945
- if (lowerKey === "headline" || lowerKey === "tool_name") continue;
1946
-
1947
- // Skip query in agent steps - it's shown in the tool call step
1948
- if (type === "agent" && lowerKey === "query") continue;
1949
-
1950
- // Special handling for thoughts - render with single lightbulb icon
1951
- // Skip reasoning
1952
- if (lowerKey === "reasoning") continue;
1953
- if (lowerKey === "thoughts" || lowerKey === "thinking" || lowerKey === "reflection") {
1954
- renderThoughts(kvpsDiv, value);
1955
- continue;
1956
- }
1957
-
1958
- // Special handling for tool_args - render only for tool/mcp types (skip for agent)
1959
- if (lowerKey === "tool_args") {
1960
- // Skip tool_args for agent steps - it's shown in the tool call step
1961
- if (type === "agent") continue;
1962
-
1963
- if (typeof value !== "object" || value === null) continue;
1964
- const argsDiv = document.createElement("div");
1965
- argsDiv.classList.add("step-tool-args");
1966
-
1967
- for (const [argKey, argValue] of Object.entries(value)) {
1968
- const argRow = document.createElement("div");
1969
- argRow.classList.add("tool-arg-row");
1970
-
1971
- const argLabel = document.createElement("span");
1972
- argLabel.classList.add("tool-arg-label");
1973
-
1974
- const iconName = extractIconFromKey(argKey);
1975
- if (iconName) {
1976
- argLabel.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
1977
- } else {
1978
- argLabel.textContent = convertToTitleCase(argKey) + ":";
1979
- }
1980
-
1981
- const argVal = document.createElement("span");
1982
- argVal.classList.add("tool-arg-value");
1983
-
1984
- const argText = cleanTextValue(argValue);
1985
-
1986
- argVal.textContent = truncateText(argText, 300);
1987
-
1988
- argRow.appendChild(argLabel);
1989
- argRow.appendChild(argVal);
1990
- argsDiv.appendChild(argRow);
1991
- }
1992
-
1993
- kvpsDiv.appendChild(argsDiv);
1994
- continue;
1995
- }
1996
-
1997
- const kvpDiv = document.createElement("div");
1998
- kvpDiv.classList.add("step-kvp");
1999
-
2000
- const keySpan = document.createElement("span");
2001
- keySpan.classList.add("step-kvp-key");
2002
-
2003
- const iconName = extractIconFromKey(key);
2004
- if (iconName) {
2005
- keySpan.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
2006
- } else {
2007
- keySpan.textContent = convertToTitleCase(key) + ":";
2008
- }
2009
-
2010
- const valueSpan = document.createElement("div");
2011
- valueSpan.classList.add("step-kvp-value");
2012
-
2013
- if (typeof value === "string" && value.startsWith("img://")) {
2014
- const imgElement = document.createElement("img");
2015
- imgElement.classList.add("screenshot-img");
2016
- imgElement.src = value.replace("img://", "/image_get?path=");
2017
- imgElement.alt = "Image Attachment";
2018
- imgElement.style.cursor = "pointer";
2019
- imgElement.style.maxWidth = "100%";
2020
- imgElement.style.display = "block";
2021
- imgElement.style.marginTop = "4px";
2022
-
2023
- // Add click handler and cursor change
2024
- imgElement.addEventListener("click", () => {
2025
- imageViewerStore.open(imgElement.src, { name: "Image Attachment" });
2026
- });
2027
-
2028
- valueSpan.appendChild(imgElement);
2029
- } else {
2030
- const valueText = cleanTextValue(value);
2031
- valueSpan.textContent = truncateText(valueText, 1000);
2032
- }
2033
-
2034
- kvpDiv.appendChild(keySpan);
2035
- kvpDiv.appendChild(valueSpan);
2036
- kvpsDiv.appendChild(kvpDiv);
2037
- }
2038
-
2039
- container.appendChild(kvpsDiv);
2040
- }
2041
-
2042
- // Add main content if present (JSON content)
2043
- if (content && content.trim()) {
2044
- const pre = document.createElement("pre");
2045
- pre.classList.add("msg-json");
2046
- pre.textContent = truncateText(content, 1000);
2047
- container.appendChild(pre);
2048
- }
2049
-}
2051
+// function renderStepDetailContent(container, content, kvps, type = null) {
2052
+// container.innerHTML = "";
2053
+
2054
+// drawKvpsIncremental(container, kvps);
2055
+
2056
+// // Special handling for response type - show content as markdown (for subordinate responses)
2057
+// if (type === "response" && content && content.trim()) {
2058
+// const responseDiv = document.createElement("div");
2059
+// responseDiv.classList.add("step-response-content");
2060
+
2061
+// // Parse markdown
2062
+// let processedContent = content;
2063
+// processedContent = convertImageTags(processedContent);
2064
+// processedContent = convertImgFilePaths(processedContent);
2065
+// processedContent = convertFilePaths(processedContent);
2066
+// processedContent = marked.parse(processedContent, { breaks: true });
2067
+// processedContent = convertPathsToLinks(processedContent);
2068
+// processedContent = addBlankTargetsToLinks(processedContent);
2069
+
2070
+// responseDiv.innerHTML = processedContent;
2071
+// container.appendChild(responseDiv);
2072
+// return;
2073
+// }
2074
+
2075
+// // Special handling for warning/error types - always show content prominently
2076
+// if ((type === "warning" || type === "error") && content && content.trim()) {
2077
+// const warningDiv = document.createElement("div");
2078
+// warningDiv.classList.add("step-warning-content");
2079
+// warningDiv.textContent = content;
2080
+// container.appendChild(warningDiv);
2081
+// // Don't return - also show kvps if present
2082
+// }
2083
+
2084
+// // Special handling for code_exe type - render as terminal-style output
2085
+// if (type === "code_exe" && kvps) {
2086
+// const runtime = kvps.runtime || kvps.Runtime || "bash";
2087
+// const code = kvps.code || kvps.Code || "";
2088
+// const output = content || "";
2089
+
2090
+// if (code || output) {
2091
+// const terminalDiv = document.createElement("div");
2092
+// terminalDiv.classList.add("step-terminal");
2093
+
2094
+// // Show output if present (no truncation - CSS handles max-height)
2095
+// if (output && output.trim()) {
2096
+// const outputPre = document.createElement("pre");
2097
+// outputPre.classList.add("terminal-output");
2098
+// // Escape HTML first, then convert paths to clickable links
2099
+// let processedOutput = escapeHTML(output);
2100
+// processedOutput = convertPathsToLinks(processedOutput);
2101
+// outputPre.innerHTML = processedOutput;
2102
+// terminalDiv.appendChild(outputPre);
2103
+// }
2104
+
2105
+// container.appendChild(terminalDiv);
2106
+// }
2107
+
2108
+// // Still render thoughts if present (but not reasoning - that's native model thinking, not structured output)
2109
+// if (kvps.thoughts || kvps.thinking) {
2110
+// const thoughtKey = kvps.thoughts ? "thoughts" : "thinking";
2111
+// const thoughtValue = kvps[thoughtKey];
2112
+// renderThoughts(container, thoughtValue);
2113
+// }
2114
+
2115
+// return;
2116
+// }
2117
+
2118
+// // Add KVPs if present
2119
+// if (kvps && Object.keys(kvps).length > 0) {
2120
+// const kvpsDiv = document.createElement("div");
2121
+// kvpsDiv.classList.add("step-kvps");
2122
+
2123
+// for (const [key, value] of Object.entries(kvps)) {
2124
+// // Skip internal/display keys
2125
+// if (key === "finished" || key === "attachments") continue;
2126
+
2127
+// // Skip code_exe specific keys that we handle specially above
2128
+// if (
2129
+// type === "code_exe" &&
2130
+// (key.toLowerCase() === "runtime" ||
2131
+// key.toLowerCase() === "session" ||
2132
+// key.toLowerCase() === "code")
2133
+// ) {
2134
+// continue;
2135
+// }
2136
+
2137
+// const lowerKey = key.toLowerCase();
2138
+
2139
+// // Skip headline and tool_name - they're shown elsewhere
2140
+// if (lowerKey === "headline" || lowerKey === "tool_name") continue;
2141
+
2142
+// // Skip query in agent steps - it's shown in the tool call step
2143
+// if (type === "agent" && lowerKey === "query") continue;
2144
+
2145
+// // Special handling for thoughts - render with single lightbulb icon
2146
+// // Skip reasoning
2147
+// if (lowerKey === "reasoning") continue;
2148
+// if (
2149
+// lowerKey === "thoughts" ||
2150
+// lowerKey === "thinking" ||
2151
+// lowerKey === "reflection"
2152
+// ) {
2153
+// renderThoughts(kvpsDiv, value);
2154
+// continue;
2155
+// }
2156
+
2157
+// // Special handling for tool_args - render only for tool/mcp types (skip for agent)
2158
+// if (lowerKey === "tool_args") {
2159
+// // Skip tool_args for agent steps - it's shown in the tool call step
2160
+// if (type === "agent") continue;
2161
+
2162
+// if (typeof value !== "object" || value === null) continue;
2163
+// const argsDiv = document.createElement("div");
2164
+// argsDiv.classList.add("step-tool-args");
2165
+
2166
+// for (const [argKey, argValue] of Object.entries(value)) {
2167
+// const argRow = document.createElement("div");
2168
+// argRow.classList.add("tool-arg-row");
2169
+
2170
+// const argLabel = document.createElement("span");
2171
+// argLabel.classList.add("tool-arg-label");
2172
+
2173
+// const iconName = extractIconFromKey(argKey);
2174
+// if (iconName) {
2175
+// argLabel.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
2176
+// } else {
2177
+// argLabel.textContent = convertToTitleCase(argKey) + ":";
2178
+// }
2179
+
2180
+// const argVal = document.createElement("span");
2181
+// argVal.classList.add("tool-arg-value");
2182
+
2183
+// const argText = cleanTextValue(argValue);
2184
+
2185
+// argVal.textContent = truncateText(argText, 300);
2186
+
2187
+// argRow.appendChild(argLabel);
2188
+// argRow.appendChild(argVal);
2189
+// argsDiv.appendChild(argRow);
2190
+// }
2191
+
2192
+// kvpsDiv.appendChild(argsDiv);
2193
+// continue;
2194
+// }
2195
+
2196
+// const kvpDiv = document.createElement("div");
2197
+// kvpDiv.classList.add("step-kvp");
2198
+
2199
+// const keySpan = document.createElement("span");
2200
+// keySpan.classList.add("step-kvp-key");
2201
+
2202
+// const iconName = extractIconFromKey(key);
2203
+// if (iconName) {
2204
+// keySpan.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
2205
+// } else {
2206
+// keySpan.textContent = convertToTitleCase(key) + ":";
2207
+// }
2208
+
2209
+// const valueSpan = document.createElement("div");
2210
+// valueSpan.classList.add("step-kvp-value");
2211
+
2212
+// if (typeof value === "string" && value.startsWith("img://")) {
2213
+// const imgElement = document.createElement("img");
2214
+// imgElement.classList.add("screenshot-img");
2215
+// imgElement.src = value.replace("img://", "/image_get?path=");
2216
+// imgElement.alt = "Image Attachment";
2217
+// imgElement.style.cursor = "pointer";
2218
+// imgElement.style.maxWidth = "100%";
2219
+// imgElement.style.display = "block";
2220
+// imgElement.style.marginTop = "4px";
2221
+
2222
+// // Add click handler and cursor change
2223
+// imgElement.addEventListener("click", () => {
2224
+// imageViewerStore.open(imgElement.src, { name: "Image Attachment" });
2225
+// });
2226
+
2227
+// valueSpan.appendChild(imgElement);
2228
+// } else {
2229
+// const valueText = cleanTextValue(value);
2230
+// valueSpan.textContent = truncateText(valueText, 1000);
2231
+// }
2232
+
2233
+// kvpDiv.appendChild(keySpan);
2234
+// kvpDiv.appendChild(valueSpan);
2235
+// kvpsDiv.appendChild(kvpDiv);
2236
+// }
2237
+
2238
+// container.appendChild(kvpsDiv);
2239
+// }
2240
+
2241
+// // Add main content if present (JSON content)
2242
+// if (content && content.trim()) {
2243
+// const pre = document.createElement("pre");
2244
+// pre.classList.add("msg-json");
2245
+// pre.textContent = truncateText(content, 1000);
2246
+// container.appendChild(pre);
2247
+// }
2248
+// }
2249
2250
/**
2251
* Helper to render thoughts/reasoning with lightbulb icon
2252
*/
2054
-function renderThoughts(container, value) {
2055
- const thoughtsDiv = document.createElement("div");
2056
- thoughtsDiv.classList.add("step-thoughts", "msg-thoughts");
2057
-
2058
- const thoughtText = cleanTextValue(value);
2059
-
2060
- if (thoughtText) {
2061
- thoughtsDiv.innerHTML = `<span class="thought-icon material-symbols-outlined">lightbulb</span><span class="thought-text">${escapeHTML(thoughtText)}</span>`;
2062
- container.appendChild(thoughtsDiv);
2063
- }
2064
-}
2253
+// function renderThoughts(container, value) {
2254
+// const thoughtsDiv = document.createElement("div");
2255
+// thoughtsDiv.classList.add("step-thoughts", "msg-thoughts");
2256
2257
+// const thoughtText = cleanTextValue(value);
2258
+
2259
+// if (thoughtText) {
2260
+// thoughtsDiv.innerHTML = `<span class="thought-icon material-symbols-outlined">lightbulb</span><span class="thought-text">${escapeHTML(thoughtText)}</span>`;
2261
+// container.appendChild(thoughtsDiv);
2262
+// }
2263
+// }
2264
2265
/**
2266
* Update process group header with step count, status, and metrics
2267
*/
2268
function updateProcessGroupHeader(group) {
2269
+ const header = group.querySelector(".process-group-header");
2270
const steps = group.querySelectorAll(".process-step");
2072
- const titleEl = group.querySelector(".group-title");
2073
- const statusEl = group.querySelector(".group-status");
2074
- const metricsEl = group.querySelector(".group-metrics");
2075
- const isCompleted = group.classList.contains("process-group-completed");
2076
-
2271
+ const titleEl = header.querySelector(".group-title");
2272
+ const badgeEl = header.querySelector(".step-badge");
2273
+ const metricsEl = header.querySelector(".group-metrics");
2274
+ const response = group.querySelector(".process-group-response");
2275
+ const isCompleted = !!response;
2276
const notificationsEl = metricsEl?.querySelector(".metric-notifications");
2078
- if (notificationsEl) {
2079
- const counts = { warning: 0, info: 0, hint: 0 };
2080
- steps.forEach((step) => {
2081
- const stepType = step.getAttribute("data-type");
2082
- if (Object.prototype.hasOwnProperty.call(counts, stepType)) {
2083
- counts[stepType] += 1;
2084
- }
2085
- });
2086
-
2087
- const totalNotifications = counts.warning + counts.info + counts.hint;
2088
- const countEl = notificationsEl.querySelector(".metric-value");
2089
- notificationsEl.classList.remove("status-wrn", "status-inf", "status-hnt");
2277
2091
- if (totalNotifications > 0) {
2092
- if (countEl) {
2093
- countEl.textContent = totalNotifications.toString();
2094
- }
2095
- if (counts.warning > 0) {
2096
- notificationsEl.classList.add("status-wrn");
2097
- } else if (counts.info > 0) {
2098
- notificationsEl.classList.add("status-inf");
2099
- } else {
2100
- notificationsEl.classList.add("status-hnt");
2101
- }
2102
- notificationsEl.hidden = false;
2103
- notificationsEl.title = `Warnings: ${counts.warning}, Info: ${counts.info}, Hints: ${counts.hint}`;
2104
- } else {
2105
- notificationsEl.hidden = true;
2106
- }
2107
- }
2108
-
2109
- // If completed, don't update metrics
2110
- if (isCompleted) {
2111
- return;
2112
- }
2113
-
2278
// Update group title with the latest agent step heading
2279
if (titleEl) {
2280
// Find the last "agent" type step
2117
- const agentSteps = Array.from(steps).filter(step => step.getAttribute("data-type") === "agent");
2281
+ const agentSteps = Array.from(steps).filter(
2282
+ (step) => step.getAttribute("data-log-type") === "agent",
2283
+ );
2284
if (agentSteps.length > 0) {
2285
const lastAgentStep = agentSteps[agentSteps.length - 1];
2120
- const lastHeading = lastAgentStep.querySelector(".step-title")?.textContent;
2286
+ const lastHeading =
2287
+ lastAgentStep.querySelector(".step-title")?.textContent;
2288
if (lastHeading) {
2289
const cleanTitle = cleanStepTitle(lastHeading, 50);
2290
if (cleanTitle) {
@@ -2126,14 +2293,26 @@ function updateProcessGroupHeader(group) {
2293
}
2294
}
2295
}
2129
-
2296
+
2297
+ // If completed, set badge to END
2298
+ if (isCompleted) {
2299
+ badgeEl.outerHTML = `<span class="step-badge END">END</span>`;
2300
+ } else {
2301
+ // if not complete, clone the last step badge
2302
+ if (badgeEl && steps.length > 0) {
2303
+ const lastStep = steps[steps.length - 1];
2304
+ const code = lastStep.getAttribute("data-step-code");
2305
+ badgeEl.outerHTML = `<span class="step-badge ${code}">${code}</span>`;
2306
+ }
2307
+ }
2308
+
2309
// Update step count in metrics - All GEN steps from all agents per process group
2310
const stepsMetricEl = metricsEl?.querySelector(".metric-steps .metric-value");
2311
if (stepsMetricEl) {
2312
const genSteps = group.querySelectorAll('.process-step[data-type="agent"]');
2313
stepsMetricEl.textContent = genSteps.length.toString();
2314
}
2136
-
2315
+
2316
// Update time metric
2317
const timeMetricEl = metricsEl?.querySelector(".metric-time .metric-value");
2318
const startTimestamp = group.getAttribute("data-start-timestamp");
@@ -2143,56 +2322,95 @@ function updateProcessGroupHeader(group) {
2322
const minutes = String(date.getMinutes()).padStart(2, "0");
2323
timeMetricEl.textContent = `${hours}:${minutes}`;
2324
}
2146
-
2325
+
2326
// Update duration metric
2148
- const durationMetricEl = metricsEl?.querySelector(".metric-duration .metric-value");
2327
+ const durationMetricEl = metricsEl?.querySelector(
2328
+ ".metric-duration .metric-value",
2329
+ );
2330
if (durationMetricEl && steps.length > 0) {
2150
- const firstTimestampMs = parseInt(steps[0]?.getAttribute("data-timestamp") || "0", 10);
2331
+ const firstTimestampMs = parseInt(
2332
+ steps[0]?.getAttribute("data-timestamp") || "0",
2333
+ 10,
2334
+ );
2335
2336
const lastStep = steps[steps.length - 1];
2153
- const lastTimestampMs = parseInt(lastStep.getAttribute("data-timestamp") || "0", 10);
2337
+ const lastTimestampMs = parseInt(
2338
+ lastStep.getAttribute("data-timestamp") || "0",
2339
+ 10,
2340
+ );
2341
2342
const totalDurationMs = Math.max(0, lastTimestampMs - firstTimestampMs);
2343
2344
durationMetricEl.textContent = formatDuration(totalDurationMs);
2345
}
2159
-
2160
- if (steps.length > 0) {
2161
- // Get the last step's type for status
2162
- const lastStep = steps[steps.length - 1];
2163
- const lastType = lastStep.getAttribute("data-type");
2164
- const lastToolName = lastStep.getAttribute("data-tool-name");
2165
- const lastTitle = lastStep.querySelector(".step-title")?.textContent || "";
2166
-
2167
- // Update status badge
2168
- if (statusEl) {
2169
- const statusCode = getStatusCode(lastType, lastToolName);
2170
- const statusColorClass = getStatusClass(lastType, lastToolName);
2346
2172
- statusEl.textContent = statusCode;
2173
- statusEl.className = `status-badge ${statusColorClass} group-status`;
2174
- }
2175
-
2176
- // Update title
2177
- if (titleEl) {
2178
- // Prefer agent type steps for the group title as they contain thinking/planning info
2179
- if (lastType === "agent" && lastTitle) {
2180
- titleEl.textContent = cleanStepTitle(lastTitle, 50);
2347
+ if (notificationsEl) {
2348
+ const counts = { warning: 0, info: 0, hint: 0 };
2349
+ steps.forEach((step) => {
2350
+ const stepType = step.getAttribute("data-type");
2351
+ if (Object.prototype.hasOwnProperty.call(counts, stepType)) {
2352
+ counts[stepType] += 1;
2353
+ }
2354
+ });
2355
+
2356
+ const totalNotifications = counts.warning + counts.info + counts.hint;
2357
+ const countEl = notificationsEl.querySelector(".metric-value");
2358
+ notificationsEl.classList.remove("status-wrn", "status-inf", "status-hnt");
2359
+
2360
+ if (totalNotifications > 0) {
2361
+ if (countEl) {
2362
+ countEl.textContent = totalNotifications.toString();
2363
+ }
2364
+ if (counts.warning > 0) {
2365
+ notificationsEl.classList.add("status-wrn");
2366
+ } else if (counts.info > 0) {
2367
+ notificationsEl.classList.add("status-inf");
2368
} else {
2182
- // Try to find the most recent agent step for a better title
2183
- const agentSteps = group.querySelectorAll('.process-step[data-type="agent"]');
2184
- if (agentSteps.length > 0) {
2185
- const lastAgentStep = agentSteps[agentSteps.length - 1];
2186
- const agentTitle = lastAgentStep.querySelector(".step-title")?.textContent || "";
2187
- if (agentTitle) {
2188
- titleEl.textContent = cleanStepTitle(agentTitle, 50);
2189
- return;
2190
- }
2191
- }
2192
- titleEl.textContent = cleanStepTitle(lastTitle, 50) || `Processing...`;
2369
+ notificationsEl.classList.add("status-hnt");
2370
}
2371
+ notificationsEl.hidden = false;
2372
+ notificationsEl.title = `Warnings: ${counts.warning}, Info: ${counts.info}, Hints: ${counts.hint}`;
2373
+ } else {
2374
+ notificationsEl.hidden = true;
2375
}
2376
}
2377
+
2378
+ if (steps.length > 0) {
2379
+ // Get the last step's type for status
2380
+ // const lastStep = steps[steps.length - 1];
2381
+ // const lastType = lastStep.getAttribute("data-type");
2382
+ // const lastToolName = lastStep.getAttribute("data-tool-name");
2383
+ // const lastTitle = lastStep.querySelector(".step-title")?.textContent || "";
2384
+ // Update status badge
2385
+ // if (statusEl) {
2386
+ // const statusCode = getStatusCode(lastType, lastToolName);
2387
+ // const statusColorClass = getStatusClass(lastType, lastToolName);
2388
+ // statusEl.textContent = statusCode;
2389
+ // statusEl.className = `step-badge ${statusColorClass} group-status`;
2390
+ // }
2391
+ // Update title
2392
+ // if (titleEl) {
2393
+ // // Prefer agent type steps for the group title as they contain thinking/planning info
2394
+ // if (lastType === "agent" && lastTitle) {
2395
+ // titleEl.textContent = cleanStepTitle(lastTitle, 50);
2396
+ // } else {
2397
+ // // Try to find the most recent agent step for a better title
2398
+ // const agentSteps = group.querySelectorAll(
2399
+ // '.process-step[data-type="agent"]',
2400
+ // );
2401
+ // if (agentSteps.length > 0) {
2402
+ // const lastAgentStep = agentSteps[agentSteps.length - 1];
2403
+ // const agentTitle =
2404
+ // lastAgentStep.querySelector(".step-title")?.textContent || "";
2405
+ // if (agentTitle) {
2406
+ // titleEl.textContent = cleanStepTitle(agentTitle, 50);
2407
+ // return;
2408
+ // }
2409
+ // }
2410
+ // titleEl.textContent = cleanStepTitle(lastTitle, 50) || `Processing...`;
2411
+ // }
2412
+ // }
2413
+ }
2414
}
2415
2416
/**
@@ -2210,49 +2428,74 @@ function truncateText(text, maxLength) {
2428
*/
2429
function markProcessGroupComplete(group, responseTitle) {
2430
if (!group) return;
2213
-
2214
- // Update status badge to END
2215
- const statusEl = group.querySelector(".group-status");
2216
- if (statusEl) {
2217
- // statusEl.innerHTML = '<span class="badge-icon material-symbols-outlined">check</span>END';
2218
- statusEl.innerHTML = 'END';
2219
- statusEl.className = "status-badge status-end group-status";
2220
- }
2221
-
2222
- // Update title if response title is available
2223
- const titleEl = group.querySelector(".group-title");
2224
- if (titleEl && responseTitle) {
2225
- const cleanTitle = cleanStepTitle(responseTitle, 50);
2226
- if (cleanTitle) {
2227
- titleEl.textContent = cleanTitle;
2228
- }
2229
- }
2230
-
2431
+
2432
+ // // Update status badge to END
2433
+ // const statusEl = group.querySelector(".group-status");
2434
+ // if (statusEl) {
2435
+ // // statusEl.innerHTML = '<span class="badge-icon material-symbols-outlined">check</span>END';
2436
+ // statusEl.innerHTML = "END";
2437
+ // statusEl.className = "step-badge status-end group-status";
2438
+ // }
2439
+
2440
+ // // Update title if response title is available
2441
+ // const titleEl = group.querySelector(".group-title");
2442
+ // if (titleEl && responseTitle) {
2443
+ // const cleanTitle = cleanStepTitle(responseTitle, 50);
2444
+ // if (cleanTitle) {
2445
+ // titleEl.textContent = cleanTitle;
2446
+ // }
2447
+ // }
2448
+
2449
// Add completed class to group
2450
group.classList.add("process-group-completed");
2233
-
2451
+
2452
// Collapse all expanded steps when processing is done (in "current" mode) with delay
2453
const detailMode = preferencesStore.detailMode;
2454
if (detailMode === "current") {
2455
// Schedule collapse for all expanded steps (deterministic)
2238
- const allExpandedSteps = group.querySelectorAll(".process-step.step-expanded");
2239
- allExpandedSteps.forEach(expandedStep => {
2456
+ const allExpandedSteps = group.querySelectorAll(
2457
+ ".process-step.step-expanded",
2458
+ );
2459
+ allExpandedSteps.forEach((expandedStep) => {
2460
scheduleStepCollapse(expandedStep, FINAL_STEP_COLLAPSE_DELAY_MS);
2461
});
2462
}
2243
-
2463
+
2464
// Calculate final duration from backend data (difference between first and last timestamps)
2465
const steps = group.querySelectorAll(".process-step");
2246
- const firstTimestampMs = parseInt(steps[0]?.getAttribute("data-timestamp") || "0", 10);
2247
- const lastTimestampMs = parseInt(steps[steps.length - 1]?.getAttribute("data-timestamp") || "0", 10);
2466
+ const firstTimestampMs = parseInt(
2467
+ steps[0]?.getAttribute("data-timestamp") || "0",
2468
+ 10,
2469
+ );
2470
+ const lastTimestampMs = parseInt(
2471
+ steps[steps.length - 1]?.getAttribute("data-timestamp") || "0",
2472
+ 10,
2473
+ );
2474
const totalDurationMs = Math.max(0, lastTimestampMs - firstTimestampMs);
2249
-
2475
+
2476
// Update duration metric with final value from backend
2477
const metricsEl = group.querySelector(".group-metrics");
2252
- const durationMetricEl = metricsEl?.querySelector(".metric-duration .metric-value");
2478
+ const durationMetricEl = metricsEl?.querySelector(
2479
+ ".metric-duration .metric-value",
2480
+ );
2481
if (durationMetricEl && totalDurationMs > 0) {
2482
durationMetricEl.textContent = formatDuration(totalDurationMs);
2483
}
2484
}
2485
2486
+// gets or creates a child DOM element
2487
+function ensureChild(parent, selector, tagName, ...classNames) {
2488
+ let el = parent.querySelector(selector);
2489
+ if (!el) {
2490
+ el = document.createElement(tagName);
2491
+ if (classNames.length) el.classList.add(...classNames);
2492
+ parent.appendChild(el);
2493
+ }
2494
+ return el;
2495
+}
2496
2497
+// returns true if this is the initial render of a chat eg. when reloading window, switching chat or catching up after a break
2498
+// returns false when already in a rendered chat and adding messages regurarly
2499
+function isMassRender() {
2500
+ return _massRender;
2501
+}