diff --git a/python/helpers/log.py b/python/helpers/log.py
index a396de0b5..d2cdcc40e 100644
--- a/python/helpers/log.py
+++ b/python/helpers/log.py
@@ -133,6 +133,9 @@ class LogItem:
id: Optional[str] = None # Add id field
guid: str = ""
timestamp: float = 0.0 # Unix timestamp in seconds
+ duration_ms: Optional[int] = None # Duration until next step (set by Log.log)
+ tokens_in: int = 0 # Input tokens consumed
+ tokens_out: int = 0 # Output tokens generated
def __post_init__(self):
self.guid = self.log.guid
@@ -146,6 +149,8 @@ class LogItem:
kvps: dict | None = None,
temp: bool | None = None,
update_progress: ProgressUpdate | None = None,
+ tokens_in: int | None = None,
+ tokens_out: int | None = None,
**kwargs,
):
if self.guid == self.log.guid:
@@ -157,9 +162,18 @@ class LogItem:
kvps=kvps,
temp=temp,
update_progress=update_progress,
+ tokens_in=tokens_in,
+ tokens_out=tokens_out,
**kwargs,
)
+ def add_tokens(self, tokens_in: int = 0, tokens_out: int = 0):
+ """Add tokens to this log item (accumulative)."""
+ if self.guid == self.log.guid:
+ self.tokens_in += tokens_in
+ self.tokens_out += tokens_out
+ self.log.updates += [self.no]
+
def stream(
self,
heading: str | None = None,
@@ -185,6 +199,9 @@ class LogItem:
"temp": self.temp,
"kvps": self.kvps,
"timestamp": self.timestamp, # Unix timestamp in seconds
+ "duration_ms": self.duration_ms, # Duration until next step
+ "tokens_in": self.tokens_in, # Input tokens
+ "tokens_out": self.tokens_out, # Output tokens
}
@@ -215,6 +232,11 @@ class Log:
no=len(self.logs),
type=type,
)
+ # Set duration on previous item and mark it as updated
+ if self.logs:
+ prev = self.logs[-1]
+ prev.duration_ms = int((item.timestamp - prev.timestamp) * 1000)
+ self.updates += [prev.no]
self.logs.append(item)
# and update it (to have just one implementation)
@@ -241,6 +263,8 @@ class Log:
temp: bool | None = None,
update_progress: ProgressUpdate | None = None,
id: Optional[str] = None,
+ tokens_in: int | None = None,
+ tokens_out: int | None = None,
**kwargs,
):
item = self.logs[no]
@@ -257,6 +281,12 @@ class Log:
if update_progress is not None:
item.update_progress = update_progress
+ if tokens_in is not None:
+ item.tokens_in = tokens_in
+
+ if tokens_out is not None:
+ item.tokens_out = tokens_out
+
# adjust all content before processing
if heading is not None:
diff --git a/webui/components/messages/process-group/process-group-store.js b/webui/components/messages/process-group/process-group-store.js
index d0f23ef7f..9382e5629 100644
--- a/webui/components/messages/process-group/process-group-store.js
+++ b/webui/components/messages/process-group/process-group-store.js
@@ -78,10 +78,10 @@ const model = {
this._persist();
},
- // Get icon for step type
+ // Get icon for step type (Material Symbols)
getStepIcon(type) {
const icons = {
- 'agent': 'psychology',
+ 'agent': 'neurology',
'tool': 'build',
'code_exe': 'terminal',
'browser': 'language',
@@ -89,23 +89,95 @@ const model = {
'hint': 'lightbulb',
'util': 'settings',
'warning': 'warning',
- 'error': 'error'
+ 'error': 'error',
+ 'mcp': 'api',
+ 'memory': 'memory',
+ 'done': 'check_circle',
+ 'subagent': 'group'
};
return icons[type] || 'circle';
},
- // Get label for step type
+ // Get 3-letter status code for step type
+ getStepCode(type) {
+ const codes = {
+ 'agent': 'GEN',
+ 'tool': 'MCP',
+ 'code_exe': 'EXE',
+ 'browser': 'EXE',
+ 'info': 'INF',
+ 'hint': 'INF',
+ 'util': 'UTL',
+ 'warning': 'WRN',
+ 'error': 'ERR',
+ 'mcp': 'MCP',
+ 'memory': 'MEM',
+ 'done': 'END',
+ 'response': 'END',
+ 'subagent': 'SUB'
+ };
+ return codes[type] || 'GEN';
+ },
+
+ // Get color class for status code
+ getStatusColorClass(type) {
+ const colors = {
+ 'agent': 'status-gen',
+ 'tool': 'status-mcp',
+ 'code_exe': 'status-exe',
+ 'browser': 'status-exe',
+ 'info': 'status-inf',
+ 'hint': 'status-inf',
+ 'util': 'status-utl',
+ 'warning': 'status-wrn',
+ 'error': 'status-err',
+ 'mcp': 'status-mcp',
+ 'memory': 'status-mem',
+ 'done': 'status-end',
+ 'response': 'status-end',
+ 'subagent': 'status-sub'
+ };
+ return colors[type] || 'status-gen';
+ },
+
+ // Get icon for badge display (Material Symbols)
+ getStepBadgeIcon(type) {
+ const icons = {
+ 'agent': 'public', // Globe for GEN (network intelligence)
+ 'tool': 'build', // Wrench for MCP
+ 'code_exe': 'terminal', // Terminal for EXE
+ 'browser': 'language', // Globe for browser
+ 'info': 'info',
+ 'hint': 'lightbulb',
+ 'util': 'settings',
+ 'warning': 'warning',
+ 'error': 'error',
+ 'mcp': 'build', // Wrench for MCP
+ 'memory': 'memory',
+ 'done': 'check_circle',
+ 'response': 'check_circle',
+ 'subagent': 'group'
+ };
+ return icons[type] || 'circle';
+ },
+
+ // Get label for step type (longer description)
getStepLabel(type) {
const labels = {
- 'agent': 'Thinking',
- 'tool': 'Tool',
- 'code_exe': 'Code',
+ 'agent': 'Generating',
+ 'tool': 'MCP Call',
+ 'code_exe': 'Executing',
'browser': 'Browser',
'info': 'Info',
'hint': 'Hint',
'util': 'Utility',
'warning': 'Warning',
- 'error': 'Error'
+ 'error': 'Error',
+ 'mcp': 'MCP',
+ 'memory': 'Memory',
+ 'done': 'Done',
+ 'response': 'Response',
+ 'subagent': 'Subagent'
};
return labels[type] || 'Process';
},
diff --git a/webui/components/messages/process-group/process-group.css b/webui/components/messages/process-group/process-group.css
index 1d3267949..d7621f820 100644
--- a/webui/components/messages/process-group/process-group.css
+++ b/webui/components/messages/process-group/process-group.css
@@ -3,11 +3,8 @@
flex-direction: column;
position: relative;
z-index: 1;
- margin: var(--spacing-sm) 0;
- padding: var(--spacing-sm) var(--spacing-md);
- border-radius: var(--border-radius);
- background: #1f3c1e;
- border: 1px solid rgba(255, 255, 255, 0.06);
+ margin: var(--spacing-xs) 0;
+ padding: var(--spacing-xs) 0;
min-width: 200px;
max-width: 100%;
box-sizing: border-box;
@@ -30,9 +27,6 @@
.message-container.has-process-group {
display: inline-flex;
flex-direction: column;
- background: #1f3c1e;
- border-radius: var(--border-radius);
- border: 1px solid rgba(255, 255, 255, 0.06);
padding: 0;
overflow: hidden;
min-width: 200px;
@@ -40,16 +34,11 @@
}
.message-container.has-process-group > .message {
- border-radius: 0 0 var(--border-radius) var(--border-radius);
border: none;
background: transparent;
margin: 0;
}
-.process-group:hover {
- border-color: rgba(255, 255, 255, 0.10);
-}
-
/* Process Group Header */
.process-group-header {
display: flex;
@@ -58,8 +47,8 @@
cursor: pointer;
user-select: none;
transition: opacity 0.15s ease;
- min-height: 24px;
- gap: var(--spacing-xs);
+ min-height: 22px;
+ gap: 6px;
white-space: nowrap;
}
@@ -67,52 +56,240 @@
opacity: 0.85;
}
+/* Expand/collapse triangle - CSS-only (no Material Icons) */
.process-group-header .expand-icon {
- font-size: 1rem;
- color: var(--color-text);
- transition: transform 0.2s ease;
+ display: inline-block;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-width: 4px 0 4px 6px;
+ border-color: transparent transparent transparent var(--color-text);
opacity: 0.5;
+ transition: transform 0.2s ease, opacity 0.15s ease;
flex-shrink: 0;
+ font-size: 0; /* Hide any text content */
+ margin-right: 2px;
+}
+
+.process-group-header:hover .expand-icon {
+ opacity: 0.8;
}
.process-group.expanded .process-group-header .expand-icon {
transform: rotate(90deg);
}
+/* Group icon removed - using status badges */
.process-group-header .group-icon {
- font-size: 0.95rem;
- color: var(--color-primary);
- opacity: 0.7;
- flex-shrink: 0;
+ display: none;
}
+/* Group title - prominent display */
.process-group-header .group-title {
- flex: 1;
+ flex: 0 1 auto;
font-size: var(--font-size-smaller);
- font-weight: 400;
+ font-weight: 500;
color: var(--color-text);
- opacity: 0.7;
+ opacity: 0.9;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ max-width: 350px;
}
+/* Step count badge */
.process-group-header .step-count {
+ font-size: 0.65rem;
+ color: var(--color-text);
+ opacity: 0.5;
+ flex-shrink: 0;
+ padding: 1px 6px;
+ font-family: var(--font-family-code);
+}
+
+/* ===========================================
+ 3-Letter Status Code Badges
+ =========================================== */
+
+/* Base status badge */
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 0.65rem;
+ font-weight: 600;
+ font-family: var(--font-family-code);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ flex-shrink: 0;
+ line-height: 1;
+}
+
+/* Status badge with icon */
+.status-badge .material-symbols-outlined {
+ font-size: 0.8rem;
+ line-height: 1;
+}
+
+/* Badge icon styling */
+.status-badge .badge-icon {
font-size: 0.7rem;
+ margin-right: 2px;
+ opacity: 0.9;
+}
+
+/* GEN - Generating (blue/cyan) */
+.status-gen {
+ background-color: rgba(56, 189, 248, 0.15);
+ color: #38bdf8;
+ border: 1px solid rgba(56, 189, 248, 0.3);
+}
+
+/* EXE - Executing (magenta/purple) */
+.status-exe {
+ background-color: rgba(186, 104, 200, 0.15);
+ color: #ba68c8;
+ border: 1px solid rgba(186, 104, 200, 0.3);
+}
+
+/* MCP - MCP Tool Call (amber/yellow) */
+.status-mcp {
+ background-color: rgba(251, 191, 36, 0.15);
+ color: #fbbf24;
+ border: 1px solid rgba(251, 191, 36, 0.3);
+}
+
+/* MEM - Memory (blue) */
+.status-mem {
+ background-color: rgba(96, 165, 250, 0.15);
+ color: #60a5fa;
+ border: 1px solid rgba(96, 165, 250, 0.3);
+}
+
+/* END - Done (green) */
+.status-end {
+ background-color: rgba(34, 197, 94, 0.15);
+ color: #22c55e;
+ border: 1px solid rgba(34, 197, 94, 0.3);
+}
+
+/* ERR - Error (red) */
+.status-err {
+ background-color: rgba(239, 68, 68, 0.15);
+ color: #ef4444;
+ border: 1px solid rgba(239, 68, 68, 0.3);
+}
+
+/* WRN - Warning (orange) */
+.status-wrn {
+ background-color: rgba(249, 115, 22, 0.15);
+ color: #f97316;
+ border: 1px solid rgba(249, 115, 22, 0.3);
+}
+
+/* SUB - Subagent (teal) */
+.status-sub {
+ background-color: rgba(20, 184, 166, 0.15);
+ color: #14b8a6;
+ border: 1px solid rgba(20, 184, 166, 0.3);
+}
+
+/* INF - Info (gray) */
+.status-inf {
+ background-color: rgba(148, 163, 184, 0.15);
+ color: #94a3b8;
+ border: 1px solid rgba(148, 163, 184, 0.3);
+}
+
+/* UTL - Utility (gray-blue) */
+.status-utl {
+ background-color: rgba(100, 116, 139, 0.15);
+ color: #64748b;
+ border: 1px solid rgba(100, 116, 139, 0.3);
+}
+
+/* Animated spinner for active status (CSS-only, no Material Icons) */
+.status-badge.status-active::after {
+ content: "";
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border: 1.5px solid currentColor;
+ border-top-color: transparent;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ margin-left: 4px;
+ flex-shrink: 0;
+}
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+/* Don't show spinner pseudo-element on END/ERR (they have their own indicators) */
+.status-badge.status-end::after,
+.status-badge.status-err::after {
+ display: none;
+}
+
+/* END status with checkmark icon (like sketch) - use ::before only */
+.status-end::before {
+ content: "✓";
+ margin-right: 2px;
+ font-weight: bold;
+}
+
+/* Completed process group styling */
+.process-group-completed {
+ opacity: 0.95;
+}
+
+.process-group-completed .group-status {
+ font-weight: 700;
+}
+
+/* Subtle completion indicator on the expand icon */
+.process-group-completed .expand-icon {
+ color: #22c55e;
+}
+
+/* Group status badge in header */
+.process-group-header .group-status {
+ margin-left: var(--spacing-xs);
+}
+
+/* Metrics row */
+.process-group-header .group-metrics {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ margin-left: auto;
+ font-size: 0.65rem;
+ font-family: var(--font-family-code);
color: var(--color-text);
opacity: 0.6;
- flex-shrink: 0;
- background-color: rgba(255, 255, 255, 0.06);
- padding: 1px 6px;
- border-radius: 8px;
}
+.process-group-header .group-metrics span[class^="metric-"] {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+}
+
+.process-group-header .group-metrics .metric-value {
+ opacity: 0.8;
+}
+
+/* Legacy timestamp/duration (for backwards compatibility) */
.process-group-header .group-timestamp {
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.65);
flex-shrink: 0;
font-family: var(--font-family-code);
- margin-left: auto;
+ display: none; /* Hidden, replaced by metrics */
}
.process-group-header .group-duration {
@@ -122,6 +299,7 @@
font-family: var(--font-family-code);
min-width: 40px;
text-align: right;
+ display: none; /* Hidden, replaced by metrics */
}
/* Process Group Content - Animated expand/collapse */
@@ -147,8 +325,8 @@
.process-group.expanded .process-group-content {
grid-template-rows: 1fr;
opacity: 1;
- margin-top: var(--spacing-sm);
- padding-top: var(--spacing-sm);
+ margin-top: var(--spacing-xs);
+ padding-top: var(--spacing-xs);
border-top-color: rgba(255, 255, 255, 0.06);
}
@@ -164,15 +342,11 @@
.process-step {
display: flex;
flex-direction: column;
- padding: var(--spacing-xxs) var(--spacing-xs);
+ padding: 1px var(--spacing-xs);
border-radius: 4px;
transition: background-color 0.15s ease;
}
-.process-step:hover {
- background-color: rgba(255, 255, 255, 0.03);
-}
-
/* Utility/Info/Hint steps have subtle background tint */
.process-step[data-type="util"],
.process-step[data-type="info"],
@@ -199,60 +373,45 @@
align-items: center;
cursor: pointer;
user-select: none;
- gap: var(--spacing-xs);
- min-height: 20px;
+ gap: 4px;
+ min-height: 18px;
}
+/* Step icon removed - using status badges instead */
.process-step-header .step-icon {
- font-size: 0.85rem;
- opacity: 0.6;
- width: 16px;
- text-align: center;
+ display: none;
}
-/* Step type colors */
-.process-step[data-type="agent"] .step-icon { color: #64b5f6; }
-.process-step[data-type="tool"] .step-icon { color: #81c784; }
-.process-step[data-type="code_exe"] .step-icon { color: #ba68c8; }
-.process-step[data-type="browser"] .step-icon { color: #ffb74d; }
-.process-step[data-type="info"] .step-icon { color: #90a4ae; }
-.process-step[data-type="util"] .step-icon { color: #78909c; }
-.process-step[data-type="hint"] .step-icon { color: #aed581; }
-.process-step[data-type="warning"] .step-icon { color: #ffd54f; }
-.process-step[data-type="error"] .step-icon { color: #e57373; }
-
+/* Step type label removed - using status badges instead */
.process-step-header .step-type {
- font-size: 0.7rem;
- font-weight: 500;
- opacity: 0.5;
- min-width: 50px;
- text-transform: uppercase;
- letter-spacing: 0.3px;
+ display: none;
}
+/* Step title */
.process-step-header .step-title {
flex: 1;
font-size: 0.75rem;
color: var(--color-text);
- opacity: 0.7;
+ opacity: 0.85;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ margin-left: var(--spacing-xs);
}
-.process-step-header .step-time {
- font-size: 0.65rem;
- color: #81c784;
- flex-shrink: 0;
- font-family: var(--font-family-code);
- min-width: 35px;
- text-align: right;
-}
-
+/* Step expand icon - CSS triangle (no Material Icons) */
.process-step-header .step-expand-icon {
- font-size: 0.8rem;
+ display: inline-block;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-width: 3px 0 3px 5px;
+ border-color: transparent transparent transparent var(--color-text);
opacity: 0.4;
transition: transform 0.2s ease, opacity 0.15s ease;
+ flex-shrink: 0;
+ font-size: 0; /* Hide any text content */
+ margin-right: 2px;
}
.process-step-header:hover .step-expand-icon {
@@ -260,7 +419,7 @@
}
.process-step.step-expanded .step-expand-icon {
- transform: rotate(180deg);
+ transform: rotate(90deg);
}
/* Step Detail Content - Animated expand/collapse */
@@ -283,16 +442,14 @@
}
.process-step-detail-content {
- padding: var(--spacing-xs) var(--spacing-sm);
+ padding: var(--spacing-xs) 0;
margin-top: var(--spacing-xxs);
margin-left: 20px; /* Align with icon */
- background-color: rgba(0, 0, 0, 0.15);
- border-radius: 4px;
+ background-color: transparent;
font-size: 0.7rem;
line-height: 1.5;
max-height: 300px;
overflow-y: auto;
- border-left: 2px solid rgba(255, 255, 255, 0.08);
}
.process-step-detail-content pre {
@@ -332,10 +489,126 @@
font-size: 0.7rem;
}
+/* Thoughts styling - single icon with plain text */
+.process-step-detail-content .step-thoughts {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ margin: var(--spacing-xs) 0;
+}
+
+.process-step-detail-content .thought-icon {
+ font-size: 0.85rem;
+ color: #fbbf24;
+ flex-shrink: 0;
+ margin-top: 2px;
+}
+
+.process-step-detail-content .thought-text {
+ font-size: 0.72rem;
+ color: var(--color-text);
+ opacity: 0.8;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+/* Light mode thoughts */
+.light-mode .process-step-detail-content .thought-icon {
+ color: #b45309;
+}
+
+/* Tool arguments structured display - clean label:value format without boxes */
+.process-step-detail-content .step-tool-args {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ margin: var(--spacing-xs) 0;
+ padding: 0;
+ background: transparent;
+ border-radius: 0;
+ border-left: none;
+}
+
+.process-step-detail-content .tool-arg-row {
+ display: flex;
+ gap: 8px;
+ align-items: baseline;
+}
+
+.process-step-detail-content .tool-arg-label {
+ font-size: 0.68rem;
+ font-weight: 600;
+ color: var(--color-primary);
+ min-width: 70px;
+ flex-shrink: 0;
+}
+
+.process-step-detail-content .tool-arg-value {
+ font-size: 0.7rem;
+ color: var(--color-text);
+ opacity: 0.85;
+ word-break: break-word;
+ font-family: var(--font-family-code);
+}
+
+/* Light mode tool args - transparent like dark mode */
+.light-mode .process-step-detail-content .step-tool-args {
+ background: transparent;
+ border-left-color: transparent;
+}
+
+/* Terminal-style output for code_exe */
+.process-step-detail-content .step-terminal {
+ margin: var(--spacing-xs) 0;
+ font-family: var(--font-family-code);
+ font-size: 0.72rem;
+}
+
+.process-step-detail-content .terminal-cmd {
+ display: flex;
+ align-items: baseline;
+ gap: 4px;
+ color: var(--color-text);
+ opacity: 0.9;
+}
+
+.process-step-detail-content .terminal-prompt {
+ color: #7ee787;
+ font-weight: 600;
+}
+
+.process-step-detail-content .terminal-code {
+ color: var(--color-text);
+ opacity: 0.85;
+}
+
+.process-step-detail-content .terminal-output {
+ margin: var(--spacing-xs) 0 0 0;
+ padding: var(--spacing-xs);
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ color: #c9d1d9;
+ white-space: pre-wrap;
+ word-break: break-word;
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+/* Light mode terminal */
+.light-mode .process-step-detail-content .terminal-prompt {
+ color: #1a7f37;
+}
+
+.light-mode .process-step-detail-content .terminal-output {
+ background: rgba(0, 0, 0, 0.05);
+ color: #24292f;
+}
+
/* Light mode adjustments */
.light-mode .process-group {
background: rgba(0, 0, 0, 0.03);
- border-color: rgba(0, 0, 0, 0.08);
+ border: none;
}
.light-mode .process-group.embedded {
@@ -361,8 +634,7 @@
}
.light-mode .process-step-detail-content {
- background-color: rgba(0, 0, 0, 0.03);
- border-left-color: rgba(0, 0, 0, 0.08);
+ background-color: transparent;
}
.light-mode .process-group-header .step-count {
@@ -371,22 +643,73 @@
/* Light mode text colors for process group */
.light-mode .process-group-header .group-title,
-.light-mode .process-group-header .step-count,
-.light-mode .process-step-header .step-title,
-.light-mode .process-step-header .step-type {
- color: #188216;
+.light-mode .process-step-header .step-title {
+ color: var(--color-text);
}
-.light-mode .process-group-header .group-timestamp {
- color: rgba(0, 0, 0, 0.5);
+.light-mode .process-group-header .group-metrics {
+ color: var(--color-text);
}
-.light-mode .process-group-header .group-duration {
- color: #2e7d32;
+/* Light mode status badge adjustments for better contrast */
+.light-mode .status-gen {
+ background-color: rgba(14, 165, 233, 0.12);
+ color: #0284c7;
+ border-color: rgba(14, 165, 233, 0.25);
}
-.light-mode .process-step-header .step-time {
- color: #388e3c;
+.light-mode .status-exe {
+ background-color: rgba(147, 51, 234, 0.12);
+ color: #7c3aed;
+ border-color: rgba(147, 51, 234, 0.25);
+}
+
+.light-mode .status-mcp {
+ background-color: rgba(217, 119, 6, 0.12);
+ color: #b45309;
+ border-color: rgba(217, 119, 6, 0.25);
+}
+
+.light-mode .status-mem {
+ background-color: rgba(37, 99, 235, 0.12);
+ color: #1d4ed8;
+ border-color: rgba(37, 99, 235, 0.25);
+}
+
+.light-mode .status-end {
+ background-color: rgba(22, 163, 74, 0.12);
+ color: #15803d;
+ border-color: rgba(22, 163, 74, 0.25);
+}
+
+.light-mode .status-err {
+ background-color: rgba(220, 38, 38, 0.12);
+ color: #b91c1c;
+ border-color: rgba(220, 38, 38, 0.25);
+}
+
+.light-mode .status-wrn {
+ background-color: rgba(234, 88, 12, 0.12);
+ color: #c2410c;
+ border-color: rgba(234, 88, 12, 0.25);
+}
+
+.light-mode .status-sub {
+ background-color: rgba(13, 148, 136, 0.12);
+ color: #0f766e;
+ border-color: rgba(13, 148, 136, 0.25);
+}
+
+.light-mode .status-inf {
+ background-color: rgba(100, 116, 139, 0.12);
+ color: #475569;
+ border-color: rgba(100, 116, 139, 0.25);
+}
+
+.light-mode .status-utl {
+ background-color: rgba(71, 85, 105, 0.12);
+ color: #334155;
+ border-color: rgba(71, 85, 105, 0.25);
}
/* Animation for loading state */
diff --git a/webui/css/messages.css b/webui/css/messages.css
index 2ee0dd3e6..76cfd2ad9 100644
--- a/webui/css/messages.css
+++ b/webui/css/messages.css
@@ -79,9 +79,12 @@
.message-user {
background-color: #4a4a4a;
+ border-radius: var(--border-radius) !important;
/* border-bottom-right-radius: var(--spacing-xxs); */
/* min-width: 195px; */
text-align: end;
+ box-shadow: inset 0 2rem 2rem -2rem rgba(0, 0, 0, 0.3),
+ inset 0 -2rem 2rem -2rem rgba(0, 0, 0, 0.1);
}
.message-user > div {
@@ -116,7 +119,7 @@
}
.message-followup .message {
- border-radius: 1.125em; /* 18px */
+ border-radius: 0;
/* border-top-left-radius: var(--spacing-xxs); */
}
@@ -137,31 +140,108 @@
.message-warning,
.message-error {
color: #e0e0e0;
+ background-color: transparent;
+ border-radius: 0;
+ box-shadow: none;
}
.message-default {
- background-color: #1a242f;
+ background-color: transparent;
}
.message-agent {
- background-color: #34506b;
+ background-color: transparent;
}
.message-agent-response {
min-width: 255px;
- background-color: #1f3c1e;
+ background-color: transparent;
}
.message-agent-delegation {
- background-color: #12685e;
+ background-color: transparent;
}
.message-tool {
- background-color: #2a4170;
+ background-color: transparent;
}
.message-code-exe {
- background-color: #4b3a69;
+ background-color: transparent;
+}
+
+/* Terminal-style code execution block */
+.message-code-exe .message-body {
+ background: #0d1117;
+ border: 1px solid rgba(48, 54, 61, 0.8);
+ border-radius: 6px;
+ padding: 0;
+ margin-top: 0.5em;
+ overflow: hidden;
+}
+
+.message-code-exe .msg-heading {
+ background: linear-gradient(180deg, #21262d 0%, #161b22 100%);
+ padding: 6px 12px;
+ border-bottom: 1px solid rgba(48, 54, 61, 0.8);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.message-code-exe .msg-heading h4 {
+ font-family: var(--font-family-code);
+ font-size: 0.75rem;
+ color: #8b949e;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.message-code-exe .msg-heading h4::before {
+ content: "$";
+ color: #7ee787;
+ font-weight: 600;
+}
+
+.message-code-exe .msg-content {
+ padding: 12px;
+ font-family: var(--font-family-code);
+ font-size: 0.72rem;
+ color: #c9d1d9;
+ line-height: 1.5;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.message-code-exe .msg-content pre {
+ margin: 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+/* Light mode terminal */
+.light-mode .message-code-exe .message-body {
+ background: #f6f8fa;
+ border-color: #d0d7de;
+}
+
+.light-mode .message-code-exe .msg-heading {
+ background: linear-gradient(180deg, #f6f8fa 0%, #eaeef2 100%);
+ border-bottom-color: #d0d7de;
+}
+
+.light-mode .message-code-exe .msg-heading h4 {
+ color: #57606a;
+}
+
+.light-mode .message-code-exe .msg-heading h4::before {
+ color: #1a7f37;
+}
+
+.light-mode .message-code-exe .msg-content {
+ color: #24292f;
}
.message-body .message-markdown-table-wrap {
@@ -181,40 +261,33 @@
white-space: break-spaces;
}
-.light-mode .message-code-exe .message-body {
- border: 1px solid var(--color-border);
-}
+/* Light mode code-exe styling moved to main terminal block above */
.message-browser {
- background-color: #4b3a69;
+ background-color: transparent;
}
.message-info {
- background-color: var(--color-panel);
+ background-color: transparent;
}
.message-util {
- background-color: #23211a;
+ background-color: transparent;
display: none;
}
.message-warning {
- background-color: #bc8036;
+ background-color: transparent;
}
.message-error {
- background-color: #af2222;
+ background-color: rgba(180, 40, 40, 0.25);
+ border: 1px solid rgba(220, 60, 60, 0.5);
+ border-radius: 8px;
+ padding: 12px;
}
-.message-code-exe .message-body {
- min-height: 5em;
- width: 100%;
- background-color: var(--color-panel);
- border-radius: 0.5em;
- margin-top: 0.5em;
- padding: 0.3em;
- font-family: var(--font-family-code);
-}
+/* Terminal styling moved to new terminal block above */
/* Agent and AI Info */
.agent-start {
@@ -254,7 +327,7 @@
}
.msg-kvps tr {
- border-bottom: 1px solid rgba(255, 255, 255, 0.15);
+ border-bottom: none;
}
.msg-heading {
@@ -415,67 +488,71 @@
}
.light-mode .msg-kvps tr {
- border-bottom: 1px solid rgb(192 192 192 / 50%);
+ border-bottom: none;
}
.light-mode .message-default {
- background-color: var(--color-panel);
+ background-color: transparent;
color: #1a242f;
}
.light-mode .message-agent {
- background-color: var(--color-panel);
+ background-color: transparent;
color: #356ca3;
}
.light-mode .message-agent-response {
- background-color: var(--color-panel);
+ background-color: transparent;
color: #188216;
}
.light-mode .message-agent-delegation {
- background-color: var(--color-panel);
+ background-color: transparent;
color: #12685e;
}
.light-mode .message-tool {
- background-color: var(--color-panel);
+ background-color: transparent;
color: #1c3c88;
}
.light-mode .message-code-exe {
- background-color: var(--color-panel);
+ background-color: transparent;
color: #6c43b0;
}
.light-mode .message-browser {
- background-color: var(--color-panel);
+ background-color: transparent;
color: #6c43b0;
}
.light-mode .message-info {
- background-color: var(--color-panel);
+ background-color: transparent;
color: #3f3f3f;
}
.light-mode .message-util {
- background-color: var(--color-panel);
+ background-color: transparent;
color: #5b5540;
}
.light-mode .message-warning {
- background-color: var(--color-panel);
+ background-color: transparent;
color: #8f4800;
}
.light-mode .message-error {
- background-color: var(--color-panel);
+ background-color: rgba(220, 60, 60, 0.15);
+ border: 1px solid rgba(180, 40, 40, 0.4);
color: #8f1010;
}
.light-mode .message-user {
background-color: var(--color-panel);
color: #4e4e4e;
+ border-radius: var(--border-radius);
+ box-shadow: inset 0 2rem 2rem -2rem rgba(0, 0, 0, 0.15),
+ inset 0 -2rem 2rem -2rem rgba(0, 0, 0, 0.05);
}
/* Markdown in messages */
@@ -604,18 +681,18 @@
}
/* 1. FIRST child’s .message – clear ONLY bottom corners */
-.message-group > *:first-child:not(:last-child) > .message {
+.message-group > *:first-child:not(:last-child) > .message-user {
border-bottom-left-radius: var(--spacing-xxs);
border-bottom-right-radius: var(--spacing-xxs);
}
/* 2. MIDDLE children’s .message – clear ALL corners */
-.message-group > *:not(:first-child):not(:last-child) > .message {
+.message-group > *:not(:first-child):not(:last-child) > .message-user {
border-radius: var(--spacing-xxs);
}
/* 3. LAST child’s .message – clear ONLY top corners */
-.message-group > *:last-child:not(:first-child) > .message {
+.message-group > *:last-child:not(:first-child) > .message-user {
border-top-left-radius: var(--spacing-xxs);
border-top-right-radius: var(--spacing-xxs);
}
@@ -630,7 +707,7 @@
.message {
/* background-color: var(--color-message-bg); */
- border-radius: var(--border-radius);
+ border-radius: 0;
padding: 0.9rem var(--spacing-sm) 0.7rem var(--spacing-sm);
overflow-x: auto;
width: auto;
@@ -639,10 +716,10 @@
/* display: block; */
word-break: break-word;
overflow-wrap: anywhere;
+ box-shadow: none;
}
/* shades */
.dark-mode .message {
- box-shadow: inset 0 2rem 2rem -2rem rgba(0, 0, 0, 0.3),
- inset 0 -2rem 2rem -2rem rgba(0, 0, 0, 0.1);
+ box-shadow: none;
}
diff --git a/webui/index.css b/webui/index.css
index 341954dce..3b58bac09 100644
--- a/webui/index.css
+++ b/webui/index.css
@@ -196,14 +196,19 @@ div#right-panel::-webkit-scrollbar-thumb:hover {
#time-date {
color: var(--color-text);
- font-size: var(--font-size-normal);
+ font-size: 1.5rem;
+ font-weight: 500;
text-align: right;
- line-height: 1.1;
+ line-height: 1.2;
+ font-family: var(--font-family-code);
}
#user-date {
- font-size: var(--font-size-small);
- opacity: 0.6;
+ font-size: 0.7rem;
+ opacity: 0.5;
+ font-weight: 400;
+ display: block;
+ margin-top: 2px;
}
/* Typography */
diff --git a/webui/index.js b/webui/index.js
index 614cd9055..3cb695496 100644
--- a/webui/index.js
+++ b/webui/index.js
@@ -185,8 +185,8 @@ async function updateUserTime() {
updateUserTime();
setInterval(updateUserTime, 1000);
-function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null) {
- const result = msgs.setMessage(id, type, heading, content, temp, kvps, timestamp);
+function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, tokensIn = 0, tokensOut = 0) {
+ const result = msgs.setMessage(id, type, heading, content, temp, kvps, timestamp, durationMs, tokensIn, tokensOut);
const chatHistoryEl = document.getElementById("chat-history");
if (preferencesStore.autoScroll && chatHistoryEl) {
chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight;
@@ -311,7 +311,10 @@ export async function poll() {
log.content,
log.temp,
log.kvps,
- log.timestamp
+ log.timestamp,
+ log.duration_ms,
+ log.tokens_in,
+ log.tokens_out
);
}
afterMessagesUpdate(response.logs);
diff --git a/webui/js/messages.js b/webui/js/messages.js
index 4acea3a2f..7bb19eb88 100644
--- a/webui/js/messages.js
+++ b/webui/js/messages.js
@@ -13,9 +13,9 @@ let messageGroup = null;
let currentProcessGroup = null; // Track current process group for collapsible UI
// Process types that should be grouped into collapsible sections
-const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'info', 'hint', 'util'];
+const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'info', 'hint', 'util', 'warning'];
// Main types that should always be visible (not collapsed)
-const MAIN_TYPES = ['user', 'response', 'warning', 'error', 'rate_limit'];
+const MAIN_TYPES = ['user', 'response', 'error', 'rate_limit'];
/**
* Check if a response is from the main agent (A0)
@@ -29,7 +29,7 @@ function isMainAgentResponse(heading) {
return match[1] === "0"; // Only A0 is the main agent
}
-export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null) {
+export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, tokensIn = 0, tokensOut = 0) {
// Check if this is a process type message
const isProcessType = PROCESS_TYPES.includes(type);
const isMainType = MAIN_TYPES.includes(type);
@@ -48,7 +48,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
if (isProcessType) {
if (processStepElement) {
// Update existing process step
- updateProcessStep(processStepElement, id, type, heading, content, kvps);
+ updateProcessStep(processStepElement, id, type, heading, content, kvps, durationMs, tokensIn, tokensOut);
return processStepElement;
}
@@ -59,14 +59,14 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
}
// Add step to current process group
- processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp);
+ processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp, durationMs, tokensIn, tokensOut);
return processStepElement;
}
// For subordinate agent responses (A1, A2, ...), treat as a process step instead of main response
if (type === "response" && !isMainAgentResponse(heading)) {
if (processStepElement) {
- updateProcessStep(processStepElement, id, "agent", heading, content, kvps);
+ updateProcessStep(processStepElement, id, "agent", heading, content, kvps, durationMs, tokensIn, tokensOut);
return processStepElement;
}
@@ -77,15 +77,18 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
}
// Add subordinate response as a step (type "agent" for appropriate styling)
- processStepElement = addProcessStep(currentProcessGroup, id, "agent", heading, content, kvps, timestamp);
+ processStepElement = addProcessStep(currentProcessGroup, id, "agent", heading, content, kvps, timestamp, durationMs, tokensIn, tokensOut);
return processStepElement;
}
- // For main agent (A0) response, embed the current process group
+ // For main agent (A0) response, embed the current process group and mark as complete
if (type === "response" && currentProcessGroup) {
const processGroupToEmbed = currentProcessGroup;
// Keep currentProcessGroup reference - subsequent process messages go to same group
+ // Mark process group as complete (END state)
+ markProcessGroupComplete(processGroupToEmbed, heading);
+
if (!messageContainer) {
// Create new container with embedded process group
messageContainer = createResponseContainerWithProcessGroup(id, processGroupToEmbed);
@@ -494,14 +497,11 @@ export function drawMessageUser(
messageDiv.className = "message message-user";
}
- // Handle heading
+ // Remove heading element if it exists (user messages no longer show label per target design)
let headingElement = messageDiv.querySelector(".msg-heading");
- if (!headingElement) {
- headingElement = document.createElement("h4");
- headingElement.classList.add("msg-heading");
- messageDiv.insertBefore(headingElement, messageDiv.firstChild);
+ if (headingElement) {
+ headingElement.remove();
}
- headingElement.innerHTML = `${heading} person`;
// Handle content
let textDiv = messageDiv.querySelector(".message-text");
@@ -1161,11 +1161,15 @@ function createProcessGroup(id) {
const header = document.createElement("div");
header.classList.add("process-group-header");
header.innerHTML = `
- chevron_right
- neurology
+
+ publicGEN
Processing...
- 0 steps
-
+
+ --:--
+ 0
+ 0s
+ --
+
`;
// Add click handler for expansion
@@ -1194,7 +1198,7 @@ function createProcessGroup(id) {
/**
* Add a step to a process group
*/
-function addProcessStep(group, id, type, heading, content, kvps, timestamp = null) {
+function addProcessStep(group, id, type, heading, content, kvps, timestamp = null, durationMs = null, tokensIn = 0, tokensOut = 0) {
const groupId = group.getAttribute("data-group-id");
const stepsContainer = group.querySelector(".process-steps");
@@ -1204,6 +1208,8 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
step.classList.add("process-step");
step.setAttribute("data-type", type);
step.setAttribute("data-step-id", id);
+ step.setAttribute("data-tokens-in", tokensIn || 0);
+ step.setAttribute("data-tokens-out", tokensOut || 0);
// Store timestamp for duration calculation
if (timestamp) {
@@ -1244,22 +1250,17 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
const stepHeader = document.createElement("div");
stepHeader.classList.add("process-step-header");
- // Calculate relative time from group start
- let relativeTimeStr = "";
- if (timestamp) {
- const groupStartTime = parseFloat(group.getAttribute("data-start-timestamp") || "0");
- if (groupStartTime > 0) {
- const relativeMs = (timestamp - groupStartTime) * 1000;
- relativeTimeStr = formatRelativeTime(relativeMs);
- }
- }
+ // Get 3-letter status code and color class
+ const statusCode = processGroupStore.getStepCode(type);
+ const statusColorClass = processGroupStore.getStatusColorClass(type);
+
+ // Get icon for step type
+ const stepIcon = processGroupStore.getStepBadgeIcon(type);
stepHeader.innerHTML = `
- ${icon}
- ${label}
+
+ ${stepIcon}${statusCode}
${escapeHTML(title)}
- ${relativeTimeStr ? `${relativeTimeStr}` : ""}
- expand_more
`;
// Add click handler for step expansion
@@ -1279,11 +1280,15 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
detailContent.classList.add("process-step-detail-content");
// Add content to detail
- renderStepDetailContent(detailContent, content, kvps);
+ renderStepDetailContent(detailContent, content, kvps, type);
detail.appendChild(detailContent);
step.appendChild(detail);
+ // Remove status-active from all previous steps (only the current step is active)
+ const prevSteps = stepsContainer.querySelectorAll(".process-step .status-badge.status-active");
+ prevSteps.forEach(badge => badge.classList.remove("status-active"));
+
stepsContainer.appendChild(step);
// Update group header
@@ -1295,7 +1300,7 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
/**
* Update an existing process step
*/
-function updateProcessStep(stepElement, id, type, heading, content, kvps) {
+function updateProcessStep(stepElement, id, type, heading, content, kvps, durationMs = null, tokensIn = 0, tokensOut = 0) {
// Update title
const titleEl = stepElement.querySelector(".step-title");
if (titleEl) {
@@ -1303,10 +1308,18 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps) {
titleEl.textContent = title;
}
+ // Update token data (use the latest values)
+ if (tokensIn > 0) {
+ stepElement.setAttribute("data-tokens-in", tokensIn);
+ }
+ if (tokensOut > 0) {
+ stepElement.setAttribute("data-tokens-out", tokensOut);
+ }
+
// Update detail content
const detailContent = stepElement.querySelector(".process-step-detail-content");
if (detailContent) {
- renderStepDetailContent(detailContent, content, kvps);
+ renderStepDetailContent(detailContent, content, kvps, type);
}
// Update parent group header
@@ -1369,9 +1382,48 @@ function cleanStepTitle(text, maxLength) {
/**
* Render content for step detail panel
*/
-function renderStepDetailContent(container, content, kvps) {
+function renderStepDetailContent(container, content, kvps, type = null) {
container.innerHTML = "";
+ // Special handling for code_exe type - render as terminal-style output
+ if (type === "code_exe" && kvps) {
+ const runtime = kvps.runtime || kvps.Runtime || "bash";
+ const code = kvps.code || kvps.Code || "";
+ const output = content || "";
+
+ if (code || output) {
+ const terminalDiv = document.createElement("div");
+ terminalDiv.classList.add("step-terminal");
+
+ // Show command line
+ if (code) {
+ const cmdLine = document.createElement("div");
+ cmdLine.classList.add("terminal-cmd");
+ cmdLine.innerHTML = `${escapeHTML(runtime)}> ${escapeHTML(code)}`;
+ terminalDiv.appendChild(cmdLine);
+ }
+
+ // Show output if present
+ if (output && output.trim()) {
+ const outputPre = document.createElement("pre");
+ outputPre.classList.add("terminal-output");
+ outputPre.textContent = truncateText(output, 1000);
+ terminalDiv.appendChild(outputPre);
+ }
+
+ container.appendChild(terminalDiv);
+ }
+
+ // Still render thoughts if present
+ if (kvps.thoughts || kvps.thinking || kvps.reasoning) {
+ const thoughtKey = kvps.thoughts ? "thoughts" : (kvps.thinking ? "thinking" : "reasoning");
+ const thoughtValue = kvps[thoughtKey];
+ renderThoughts(container, thoughtValue);
+ }
+
+ return;
+ }
+
// Add KVPs if present
if (kvps && Object.keys(kvps).length > 0) {
const kvpsDiv = document.createElement("div");
@@ -1381,20 +1433,79 @@ function renderStepDetailContent(container, content, kvps) {
// Skip internal/display keys
if (key === "finished" || key === "attachments") continue;
- const kvpDiv = document.createElement("div");
- kvpDiv.classList.add("step-kvp");
+ // Skip code_exe specific keys that we handle specially above
+ if (type === "code_exe" && (key.toLowerCase() === "runtime" || key.toLowerCase() === "session" || key.toLowerCase() === "code")) {
+ continue;
+ }
- // Add msg-thoughts class for thoughts-related keys (controlled by showThoughts preference)
const lowerKey = key.toLowerCase();
- if (lowerKey === "thoughts" || lowerKey === "thinking" || lowerKey === "reflection") {
- kvpDiv.classList.add("msg-thoughts");
+ // Special handling for thoughts/reasoning - render with single lightbulb icon
+ if (lowerKey === "thoughts" || lowerKey === "thinking" || lowerKey === "reflection" || lowerKey === "reasoning") {
+ const thoughtsDiv = document.createElement("div");
+ thoughtsDiv.classList.add("step-thoughts", "msg-thoughts");
+
// Apply current preference state - hide if showThoughts is false
if (!preferencesStore.showThoughts) {
- kvpDiv.classList.add("hide-thoughts");
+ thoughtsDiv.classList.add("hide-thoughts");
}
+
+ let thoughtText = value;
+ if (typeof value === "object") {
+ // Handle array of thoughts
+ if (Array.isArray(value)) {
+ thoughtText = value.filter(t => t && String(t).trim() && String(t).trim() !== "[" && String(t).trim() !== "]").join("\n");
+ } else {
+ thoughtText = JSON.stringify(value, null, 2);
+ }
+ }
+
+ // Clean up the text - remove standalone brackets
+ thoughtText = String(thoughtText).replace(/^\s*[\[\]]\s*$/gm, "").trim();
+
+ if (thoughtText) {
+ // Single icon + text block
+ thoughtsDiv.innerHTML = `lightbulb${escapeHTML(thoughtText)}`;
+ }
+
+ kvpsDiv.appendChild(thoughtsDiv);
+ continue;
}
+ // Special handling for tool_args - render as structured labeled parameters
+ if (lowerKey === "tool_args" && typeof value === "object" && value !== null) {
+ const argsDiv = document.createElement("div");
+ argsDiv.classList.add("step-tool-args");
+
+ for (const [argKey, argValue] of Object.entries(value)) {
+ const argRow = document.createElement("div");
+ argRow.classList.add("tool-arg-row");
+
+ const argLabel = document.createElement("span");
+ argLabel.classList.add("tool-arg-label");
+ argLabel.textContent = convertToTitleCase(argKey) + ":";
+
+ const argVal = document.createElement("span");
+ argVal.classList.add("tool-arg-value");
+
+ let argText = argValue;
+ if (typeof argValue === "object") {
+ argText = JSON.stringify(argValue, null, 2);
+ }
+ argVal.textContent = truncateText(String(argText), 300);
+
+ argRow.appendChild(argLabel);
+ argRow.appendChild(argVal);
+ argsDiv.appendChild(argRow);
+ }
+
+ kvpsDiv.appendChild(argsDiv);
+ continue;
+ }
+
+ const kvpDiv = document.createElement("div");
+ kvpDiv.classList.add("step-kvp");
+
const keySpan = document.createElement("span");
keySpan.classList.add("step-kvp-key");
keySpan.textContent = convertToTitleCase(key) + ":";
@@ -1431,39 +1542,124 @@ function renderStepDetailContent(container, content, kvps) {
}
/**
- * Update process group header with step count and status
+ * Helper to render thoughts/reasoning with lightbulb icon
+ */
+function renderThoughts(container, value) {
+ const thoughtsDiv = document.createElement("div");
+ thoughtsDiv.classList.add("step-thoughts", "msg-thoughts");
+
+ // Apply current preference state - hide if showThoughts is false
+ if (!preferencesStore.showThoughts) {
+ thoughtsDiv.classList.add("hide-thoughts");
+ }
+
+ let thoughtText = value;
+ if (typeof value === "object") {
+ if (Array.isArray(value)) {
+ thoughtText = value.filter(t => t && String(t).trim() && String(t).trim() !== "[" && String(t).trim() !== "]").join("\n");
+ } else {
+ thoughtText = JSON.stringify(value, null, 2);
+ }
+ }
+
+ thoughtText = String(thoughtText).replace(/^\s*[\[\]]\s*$/gm, "").trim();
+
+ if (thoughtText) {
+ thoughtsDiv.innerHTML = `lightbulb${escapeHTML(thoughtText)}`;
+ container.appendChild(thoughtsDiv);
+ }
+}
+
+/**
+ * Update process group header with step count, status, and metrics
*/
function updateProcessGroupHeader(group) {
const steps = group.querySelectorAll(".process-step");
- const countEl = group.querySelector(".step-count");
const titleEl = group.querySelector(".group-title");
+ const statusEl = group.querySelector(".group-status");
+ const metricsEl = group.querySelector(".group-metrics");
- if (countEl) {
- const count = steps.length;
- countEl.textContent = `${count} step${count !== 1 ? "s" : ""}`;
+ // Update step count in metrics
+ const stepsMetricEl = metricsEl?.querySelector(".metric-steps .metric-value");
+ if (stepsMetricEl) {
+ stepsMetricEl.textContent = steps.length.toString();
}
- if (titleEl && steps.length > 0) {
- // Get the last step's type for the title
+ // Update time metric
+ const timeMetricEl = metricsEl?.querySelector(".metric-time .metric-value");
+ const startTimestamp = group.getAttribute("data-start-timestamp");
+ if (timeMetricEl && startTimestamp) {
+ const date = new Date(parseFloat(startTimestamp) * 1000);
+ const hours = String(date.getHours()).padStart(2, "0");
+ const minutes = String(date.getMinutes()).padStart(2, "0");
+ timeMetricEl.textContent = `${hours}:${minutes}`;
+ }
+
+ // Update duration metric (elapsed time since start)
+ const durationMetricEl = metricsEl?.querySelector(".metric-duration .metric-value");
+ if (durationMetricEl && startTimestamp) {
+ const startMs = parseFloat(startTimestamp) * 1000;
+ const elapsedMs = Date.now() - startMs;
+ if (elapsedMs < 60000) {
+ durationMetricEl.textContent = `${Math.round(elapsedMs / 1000)}s`;
+ } else {
+ const mins = Math.floor(elapsedMs / 60000);
+ const secs = Math.round((elapsedMs % 60000) / 1000);
+ durationMetricEl.textContent = `${mins}m${secs}s`;
+ }
+ }
+
+ // Update tokens metric (aggregate from all steps)
+ const tokensMetricEl = metricsEl?.querySelector(".metric-tokens .metric-value");
+ if (tokensMetricEl) {
+ let totalTokensIn = 0;
+ let totalTokensOut = 0;
+ steps.forEach(step => {
+ totalTokensIn += parseInt(step.getAttribute("data-tokens-in") || 0, 10);
+ totalTokensOut += parseInt(step.getAttribute("data-tokens-out") || 0, 10);
+ });
+ const totalTokens = totalTokensIn + totalTokensOut;
+ if (totalTokens > 0) {
+ // Format as compact notation (e.g., 20k/3k for input/output)
+ tokensMetricEl.textContent = formatTokenCount(totalTokensIn, totalTokensOut);
+ } else {
+ tokensMetricEl.textContent = "--";
+ }
+ }
+
+ if (steps.length > 0) {
+ // Get the last step's type for status
const lastStep = steps[steps.length - 1];
const lastType = lastStep.getAttribute("data-type");
const lastTitle = lastStep.querySelector(".step-title")?.textContent || "";
- // Prefer agent type steps for the group title as they contain thinking/planning info
- if (lastType === "agent" && lastTitle) {
- titleEl.textContent = cleanStepTitle(lastTitle, 50);
- } else {
- // Try to find the most recent agent step for a better title
- const agentSteps = group.querySelectorAll('.process-step[data-type="agent"]');
- if (agentSteps.length > 0) {
- const lastAgentStep = agentSteps[agentSteps.length - 1];
- const agentTitle = lastAgentStep.querySelector(".step-title")?.textContent || "";
- if (agentTitle) {
- titleEl.textContent = cleanStepTitle(agentTitle, 50);
- return;
+ // Update status badge with icon (keep status-active during execution)
+ if (statusEl) {
+ const statusCode = processGroupStore.getStepCode(lastType);
+ const statusColorClass = processGroupStore.getStatusColorClass(lastType);
+ const badgeIcon = processGroupStore.getStepBadgeIcon(lastType);
+ statusEl.innerHTML = `${badgeIcon}${statusCode}`;
+ statusEl.className = `status-badge ${statusColorClass} status-active group-status`;
+ }
+
+ // Update title
+ if (titleEl) {
+ // Prefer agent type steps for the group title as they contain thinking/planning info
+ if (lastType === "agent" && lastTitle) {
+ titleEl.textContent = cleanStepTitle(lastTitle, 50);
+ } else {
+ // Try to find the most recent agent step for a better title
+ const agentSteps = group.querySelectorAll('.process-step[data-type="agent"]');
+ if (agentSteps.length > 0) {
+ const lastAgentStep = agentSteps[agentSteps.length - 1];
+ const agentTitle = lastAgentStep.querySelector(".step-title")?.textContent || "";
+ if (agentTitle) {
+ titleEl.textContent = cleanStepTitle(agentTitle, 50);
+ return;
+ }
}
+ titleEl.textContent = cleanStepTitle(lastTitle, 50) || `Processing...`;
}
- titleEl.textContent = `Processing (${processGroupStore.getStepLabel(lastType)})`;
}
}
}
@@ -1478,6 +1674,52 @@ function truncateText(text, maxLength) {
return text.substring(0, maxLength - 3) + "...";
}
+/**
+ * Mark a process group as complete (END state)
+ */
+function markProcessGroupComplete(group, responseTitle) {
+ if (!group) return;
+
+ // Update status badge to END (remove status-active)
+ const statusEl = group.querySelector(".group-status");
+ if (statusEl) {
+ statusEl.textContent = "END";
+ statusEl.className = "status-badge status-end group-status"; // No status-active
+ }
+
+ // Remove status-active from all step badges (stop spinners)
+ const stepBadges = group.querySelectorAll(".process-step .status-badge.status-active");
+ stepBadges.forEach(badge => badge.classList.remove("status-active"));
+
+ // Update title if response title is available
+ const titleEl = group.querySelector(".group-title");
+ if (titleEl && responseTitle) {
+ const cleanTitle = cleanStepTitle(responseTitle, 50);
+ if (cleanTitle) {
+ titleEl.textContent = cleanTitle;
+ }
+ }
+
+ // Add completed class to group
+ group.classList.add("process-group-completed");
+
+ // Update duration to final value
+ const metricsEl = group.querySelector(".group-metrics");
+ const durationMetricEl = metricsEl?.querySelector(".metric-duration .metric-value");
+ const startTimestamp = group.getAttribute("data-start-timestamp");
+ if (durationMetricEl && startTimestamp) {
+ const startMs = parseFloat(startTimestamp) * 1000;
+ const elapsedMs = Date.now() - startMs;
+ if (elapsedMs < 60000) {
+ durationMetricEl.textContent = `${Math.round(elapsedMs / 1000)}s`;
+ } else {
+ const mins = Math.floor(elapsedMs / 60000);
+ const secs = Math.round((elapsedMs % 60000) / 1000);
+ durationMetricEl.textContent = `${mins}m${secs}s`;
+ }
+ }
+}
+
/**
* Reset process group state (called on context switch)
*/
@@ -1486,6 +1728,26 @@ export function resetProcessGroups() {
messageGroup = null;
}
+/**
+ * Format token counts in compact notation (e.g., "12k/3k" for input/output)
+ */
+function formatTokenCount(tokensIn, tokensOut) {
+ const formatCompact = (n) => {
+ if (n >= 1000000) return `${(n / 1000000).toFixed(1)}m`;
+ if (n >= 1000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k`;
+ return n.toString();
+ };
+
+ if (tokensIn > 0 && tokensOut > 0) {
+ return `${formatCompact(tokensIn)}/${formatCompact(tokensOut)}`;
+ } else if (tokensIn > 0) {
+ return `${formatCompact(tokensIn)}↓`;
+ } else if (tokensOut > 0) {
+ return `${formatCompact(tokensOut)}↑`;
+ }
+ return "--";
+}
+
/**
* Format Unix timestamp as date-time string (YYYY-MM-DD HH:MM:SS)
*/
@@ -1500,18 +1762,3 @@ function formatDateTime(timestamp) {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
-/**
- * Format relative time for steps (e.g., +0.5s, +2.3s)
- */
-function formatRelativeTime(ms) {
- if (ms < 100) {
- return "+0s";
- }
- const seconds = ms / 1000;
- if (seconds < 60) {
- return `+${seconds.toFixed(1)}s`;
- }
- const minutes = Math.floor(seconds / 60);
- const remainingSeconds = Math.round(seconds % 60);
- return `+${minutes}m${remainingSeconds}s`;
-}