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