message rendering polishing
frdel committed
Feb 4, 2026 at 08:47 UTC
fe46f045fef64ac83d9f0edc2b79115f34eb25d7
9 files changed
+454
-393
agent.py
+1
-3
@@ -590,7 +590,7 @@ class Agent:
590
error_message = errors.format_error(e)
591
592
self.context.log.log(
593
- type="warning", content="Critical error occurred, retrying..."
593
+ type="warning", heading="Critical error occurred, retrying...", content=error_message
594
)
595
PrintStyle(font_color="orange", padding=True).print(
596
"Critical error occurred, retrying..."
@@ -626,9 +626,7 @@ class Agent:
626
PrintStyle(font_color="red", padding=True).print(error_message)
627
self.context.log.log(
628
type="error",
629
- heading="Error",
629
content=error_message,
631
- kvps={"text": error_text},
630
)
631
PrintStyle(font_color="red", padding=True).print(
632
f"{self.agent_name}: {error_text}"
python/extensions/monologue_end/_50_memorize_fragments.py
+144
-146
@@ -1,5 +1,5 @@
1
import asyncio
2
-from python.helpers import settings
2
+from python.helpers import settings, errors
3
from python.helpers.extension import Extension
4
from python.helpers.memory import Memory
5
from python.helpers.dirty_json import DirtyJson
@@ -33,168 +33,166 @@ class MemorizeMemories(Extension):
33
34
async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
35
36
+ try:
37
+ set = settings.get_settings()
38
37
- await asyncio.sleep(15)
38
-
39
- set = settings.get_settings()
40
-
41
- db = await Memory.get(self.agent)
42
-
43
- # get system message and chat history for util llm
44
- system = self.agent.read_prompt("memory.memories_sum.sys.md")
45
- msgs_text = self.agent.concat_messages(self.agent.history)
46
-
47
- # # log query streamed by LLM
48
- # async def log_callback(content):
49
- # log_item.stream(content=content)
50
-
51
- # call util llm to find info in history
52
- memories_json = await self.agent.call_utility_model(
53
- system=system,
54
- message=msgs_text,
55
- # callback=log_callback,
56
- background=True,
57
- )
58
-
59
- # log data < no need for streaming utility messages
60
- log_item.update(content=memories_json)
61
-
62
- # Add validation and error handling for memories_json
63
- if not memories_json or not isinstance(memories_json, str):
64
- log_item.update(heading="No response from utility model.")
65
- return
39
+ db = await Memory.get(self.agent)
40
67
- # Strip any whitespace that might cause issues
68
- memories_json = memories_json.strip()
41
+ # get system message and chat history for util llm
42
+ system = self.agent.read_prompt("memory.memories_sum.sys.md")
43
+ msgs_text = self.agent.concat_messages(self.agent.history)
44
70
- if not memories_json:
71
- log_item.update(heading="Empty response from utility model.")
72
- return
45
+ # # log query streamed by LLM
46
+ # async def log_callback(content):
47
+ # log_item.stream(content=content)
48
74
- try:
75
- memories = DirtyJson.parse_string(memories_json)
76
- except Exception as e:
77
- log_item.update(heading=f"Failed to parse memories response: {str(e)}")
78
- return
49
+ # call util llm to find info in history
50
+ memories_json = await self.agent.call_utility_model(
51
+ system=system,
52
+ message=msgs_text,
53
+ # callback=log_callback,
54
+ background=True,
55
+ )
56
80
- # Validate that memories is a list or convertible to one
81
- if memories is None:
82
- log_item.update(heading="No valid memories found in response.")
83
- return
57
+ # log data < no need for streaming utility messages
58
+ log_item.update(content=memories_json)
59
85
- # If memories is not a list, try to make it one
86
- if not isinstance(memories, list):
87
- if isinstance(memories, (str, dict)):
88
- memories = [memories]
89
- else:
90
- log_item.update(heading="Invalid memories format received.")
60
+ # Add validation and error handling for memories_json
61
+ if not memories_json or not isinstance(memories_json, str):
62
+ log_item.update(heading="No response from utility model.")
63
return
64
93
- if not isinstance(memories, list) or len(memories) == 0:
94
- log_item.update(heading="No useful information to memorize.")
95
- return
96
- else:
97
- memories_txt = "\n\n".join([str(memory) for memory in memories]).strip()
98
- log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt)
99
-
100
- # Process memories with intelligent consolidation
101
- total_processed = 0
102
- total_consolidated = 0
103
- rem = []
65
+ # Strip any whitespace that might cause issues
66
+ memories_json = memories_json.strip()
67
105
- for memory in memories:
106
- # Convert memory to plain text
107
- txt = f"{memory}"
68
+ if not memories_json:
69
+ log_item.update(heading="Empty response from utility model.")
70
+ return
71
109
- if set["memory_memorize_consolidation"]:
110
-
111
- try:
112
- # Use intelligent consolidation system
113
- from python.helpers.memory_consolidation import create_memory_consolidator
114
- consolidator = create_memory_consolidator(
115
- self.agent,
116
- similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
117
- max_similar_memories=8,
118
- max_llm_context_memories=4
119
- )
72
+ try:
73
+ memories = DirtyJson.parse_string(memories_json)
74
+ except Exception as e:
75
+ log_item.update(heading=f"Failed to parse memories response: {str(e)}")
76
+ return
77
121
- # Create memory item-specific log for detailed tracking
122
- memory_log = None # too many utility messages, skip log for now
123
- # memory_log = self.agent.context.log.log(
124
- # type="util",
125
- # heading=f"Processing memory fragment: {txt[:50]}...",
126
- # update_progress="none" # Don't affect status bar
127
- # )
128
-
129
- # Process with intelligent consolidation
130
- result_obj = await consolidator.process_new_memory(
131
- new_memory=txt,
132
- area=Memory.Area.FRAGMENTS.value,
133
- metadata={"area": Memory.Area.FRAGMENTS.value},
134
- log_item=memory_log
135
- )
78
+ # Validate that memories is a list or convertible to one
79
+ if memories is None:
80
+ log_item.update(heading="No valid memories found in response.")
81
+ return
82
137
- # Update the individual log item with completion status but keep it temporary
138
- if result_obj.get("success"):
139
- total_consolidated += 1
140
- if memory_log:
141
- memory_log.update(
142
- result="Fragment processed successfully",
143
- heading=f"Memory fragment completed: {txt[:50]}...",
144
- update_progress="none" # Show briefly then disappear
145
- )
146
- else:
147
- if memory_log:
148
- memory_log.update(
149
- result="Fragment processing failed",
150
- heading=f"Memory fragment failed: {txt[:50]}...",
151
- update_progress="none" # Show briefly then disappear
152
- )
153
- total_processed += 1
154
-
155
- except Exception as e:
156
- # Log error but continue processing
157
- log_item.update(consolidation_error=str(e))
158
- total_processed += 1
159
-
160
- # Update final results with structured logging
161
- log_item.update(
162
- heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated",
163
- memories=memories_txt,
164
- result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated",
165
- memories_processed=total_processed,
166
- memories_consolidated=total_consolidated,
167
- update_progress="none"
168
- )
83
+ # If memories is not a list, try to make it one
84
+ if not isinstance(memories, list):
85
+ if isinstance(memories, (str, dict)):
86
+ memories = [memories]
87
+ else:
88
+ log_item.update(heading="Invalid memories format received.")
89
+ return
90
91
+ if not isinstance(memories, list) or len(memories) == 0:
92
+ log_item.update(heading="No useful information to memorize.")
93
+ return
94
else:
95
+ memories_txt = "\n\n".join([str(memory) for memory in memories]).strip()
96
+ log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt)
97
+
98
+ # Process memories with intelligent consolidation
99
+ total_processed = 0
100
+ total_consolidated = 0
101
+ rem = []
102
+
103
+ for memory in memories:
104
+ # Convert memory to plain text
105
+ txt = f"{memory}"
106
+
107
+ if set["memory_memorize_consolidation"]:
108
+
109
+ try:
110
+ # Use intelligent consolidation system
111
+ from python.helpers.memory_consolidation import create_memory_consolidator
112
+ consolidator = create_memory_consolidator(
113
+ self.agent,
114
+ similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
115
+ max_similar_memories=8,
116
+ max_llm_context_memories=4
117
+ )
118
+
119
+ # Create memory item-specific log for detailed tracking
120
+ memory_log = None # too many utility messages, skip log for now
121
+ # memory_log = self.agent.context.log.log(
122
+ # type="util",
123
+ # heading=f"Processing memory fragment: {txt[:50]}...",
124
+ # update_progress="none" # Don't affect status bar
125
+ # )
126
+
127
+ # Process with intelligent consolidation
128
+ result_obj = await consolidator.process_new_memory(
129
+ new_memory=txt,
130
+ area=Memory.Area.FRAGMENTS.value,
131
+ metadata={"area": Memory.Area.FRAGMENTS.value},
132
+ log_item=memory_log
133
+ )
134
+
135
+ # Update the individual log item with completion status but keep it temporary
136
+ if result_obj.get("success"):
137
+ total_consolidated += 1
138
+ if memory_log:
139
+ memory_log.update(
140
+ result="Fragment processed successfully",
141
+ heading=f"Memory fragment completed: {txt[:50]}...",
142
+ update_progress="none" # Show briefly then disappear
143
+ )
144
+ else:
145
+ if memory_log:
146
+ memory_log.update(
147
+ result="Fragment processing failed",
148
+ heading=f"Memory fragment failed: {txt[:50]}...",
149
+ update_progress="none" # Show briefly then disappear
150
+ )
151
+ total_processed += 1
152
+
153
+ except Exception as e:
154
+ # Log error but continue processing
155
+ log_item.update(consolidation_error=str(e))
156
+ total_processed += 1
157
+
158
+ # Update final results with structured logging
159
+ log_item.update(
160
+ heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated",
161
+ memories=memories_txt,
162
+ result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated",
163
+ memories_processed=total_processed,
164
+ memories_consolidated=total_consolidated,
165
+ update_progress="none"
166
+ )
167
172
- # remove previous fragments too similiar to this one
173
- if set["memory_memorize_replace_threshold"] > 0:
174
- rem += await db.delete_documents_by_query(
175
- query=txt,
176
- threshold=set["memory_memorize_replace_threshold"],
177
- filter=f"area=='{Memory.Area.FRAGMENTS.value}'",
168
+ else:
169
+
170
+ # remove previous fragments too similiar to this one
171
+ if set["memory_memorize_replace_threshold"] > 0:
172
+ rem += await db.delete_documents_by_query(
173
+ query=txt,
174
+ threshold=set["memory_memorize_replace_threshold"],
175
+ filter=f"area=='{Memory.Area.FRAGMENTS.value}'",
176
+ )
177
+ if rem:
178
+ rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
179
+ log_item.update(replaced=rem_txt)
180
+
181
+ # insert new memory
182
+ await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
183
+
184
+ log_item.update(
185
+ result=f"{len(memories)} entries memorized.",
186
+ heading=f"{len(memories)} entries memorized.",
187
)
188
if rem:
180
- rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
181
- log_item.update(replaced=rem_txt)
182
-
183
- # insert new memory
184
- await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
185
-
186
- log_item.update(
187
- result=f"{len(memories)} entries memorized.",
188
- heading=f"{len(memories)} entries memorized.",
189
- )
190
- if rem:
191
- log_item.stream(result=f"\nReplaced {len(rem)} previous memories.")
192
-
189
+ log_item.stream(result=f"\nReplaced {len(rem)} previous memories.")
190
+
191
192
193
196
- # except Exception as e:
197
- # err = errors.format_error(e)
198
- # self.agent.context.log.log(
199
- # type="error", heading="Memorize memories extension error:", content=err
200
- # )
194
+ except Exception as e:
195
+ err = errors.format_error(e)
196
+ self.agent.context.log.log(
197
+ type="warning", heading="Memorize memories extension error", content=err
198
+ )
python/extensions/monologue_end/_51_memorize_solutions.py
+147
-147
@@ -1,5 +1,5 @@
1
import asyncio
2
-from python.helpers import settings
2
+from python.helpers import settings, errors
3
from python.helpers.extension import Extension
4
from python.helpers.memory import Memory
5
from python.helpers.dirty_json import DirtyJson
@@ -31,171 +31,171 @@ class MemorizeSolutions(Extension):
31
return task
32
33
async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
34
+ try:
35
+ set = settings.get_settings()
36
35
- set = settings.get_settings()
36
-
37
- db = await Memory.get(self.agent)
38
-
39
- # get system message and chat history for util llm
40
- system = self.agent.read_prompt("memory.solutions_sum.sys.md")
41
- msgs_text = self.agent.concat_messages(self.agent.history)
37
+ db = await Memory.get(self.agent)
38
43
- # log query streamed by LLM
44
- # async def log_callback(content):
45
- # log_item.stream(content=content)
39
+ # get system message and chat history for util llm
40
+ system = self.agent.read_prompt("memory.solutions_sum.sys.md")
41
+ msgs_text = self.agent.concat_messages(self.agent.history)
42
47
- # call util llm to find solutions in history
48
- solutions_json = await self.agent.call_utility_model(
49
- system=system,
50
- message=msgs_text,
51
- # callback=log_callback,
52
- background=True,
53
- )
43
+ # log query streamed by LLM
44
+ # async def log_callback(content):
45
+ # log_item.stream(content=content)
46
55
- # log query < no need for streaming utility messages
56
- log_item.update(content=solutions_json)
47
+ # call util llm to find solutions in history
48
+ solutions_json = await self.agent.call_utility_model(
49
+ system=system,
50
+ message=msgs_text,
51
+ # callback=log_callback,
52
+ background=True,
53
+ )
54
55
+ # log query < no need for streaming utility messages
56
+ log_item.update(content=solutions_json)
57
58
60
- # Add validation and error handling for solutions_json
61
- if not solutions_json or not isinstance(solutions_json, str):
62
- log_item.update(heading="No response from utility model.")
63
- return
59
65
- # Strip any whitespace that might cause issues
66
- solutions_json = solutions_json.strip()
60
+ # Add validation and error handling for solutions_json
61
+ if not solutions_json or not isinstance(solutions_json, str):
62
+ log_item.update(heading="No response from utility model.")
63
+ return
64
68
- if not solutions_json:
69
- log_item.update(heading="Empty response from utility model.")
70
- return
65
+ # Strip any whitespace that might cause issues
66
+ solutions_json = solutions_json.strip()
67
72
- try:
73
- solutions = DirtyJson.parse_string(solutions_json)
74
- except Exception as e:
75
- log_item.update(heading=f"Failed to parse solutions response: {str(e)}")
76
- return
68
+ if not solutions_json:
69
+ log_item.update(heading="Empty response from utility model.")
70
+ return
71
78
- # Validate that solutions is a list or convertible to one
79
- if solutions is None:
80
- log_item.update(heading="No valid solutions found in response.")
81
- return
72
+ try:
73
+ solutions = DirtyJson.parse_string(solutions_json)
74
+ except Exception as e:
75
+ log_item.update(heading=f"Failed to parse solutions response: {str(e)}")
76
+ return
77
83
- # If solutions is not a list, try to make it one
84
- if not isinstance(solutions, list):
85
- if isinstance(solutions, (str, dict)):
86
- solutions = [solutions]
87
- else:
88
- log_item.update(heading="Invalid solutions format received.")
78
+ # Validate that solutions is a list or convertible to one
79
+ if solutions is None:
80
+ log_item.update(heading="No valid solutions found in response.")
81
return
82
91
- if not isinstance(solutions, list) or len(solutions) == 0:
92
- log_item.update(heading="No successful solutions to memorize.")
93
- return
94
- else:
95
- solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip()
96
- log_item.update(
97
- heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt
98
- )
83
+ # If solutions is not a list, try to make it one
84
+ if not isinstance(solutions, list):
85
+ if isinstance(solutions, (str, dict)):
86
+ solutions = [solutions]
87
+ else:
88
+ log_item.update(heading="Invalid solutions format received.")
89
+ return
90
100
- # Process solutions with intelligent consolidation
101
- total_processed = 0
102
- total_consolidated = 0
103
- rem = []
104
-
105
- for solution in solutions:
106
- # Convert solution to structured text
107
- if isinstance(solution, dict):
108
- problem = solution.get('problem', 'Unknown problem')
109
- solution_text = solution.get('solution', 'Unknown solution')
110
- txt = f"# Problem\n {problem}\n# Solution\n {solution_text}"
91
+ if not isinstance(solutions, list) or len(solutions) == 0:
92
+ log_item.update(heading="No successful solutions to memorize.")
93
+ return
94
else:
112
- # If solution is not a dict, convert it to string
113
- txt = f"# Solution\n {str(solution)}"
114
-
115
- if set["memory_memorize_consolidation"]:
116
- try:
117
- # Use intelligent consolidation system
118
- from python.helpers.memory_consolidation import create_memory_consolidator
119
- consolidator = create_memory_consolidator(
120
- self.agent,
121
- similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
122
- max_similar_memories=6, # Fewer for solutions (more complex)
123
- max_llm_context_memories=3
124
- )
125
-
126
- # Create solution-specific log for detailed tracking
127
- solution_log = None # too many utility messages, skip log for now
128
- # solution_log = self.agent.context.log.log(
129
- # type="util",
130
- # heading=f"Processing solution: {txt[:50]}...",
131
- # update_progress="none" # Don't affect status bar
132
- # )
133
-
134
- # Process with intelligent consolidation
135
- result_obj = await consolidator.process_new_memory(
136
- new_memory=txt,
137
- area=Memory.Area.SOLUTIONS.value,
138
- metadata={"area": Memory.Area.SOLUTIONS.value},
139
- log_item=solution_log
140
- )
141
-
142
- # Update the individual log item with completion status but keep it temporary
143
- if result_obj.get("success"):
144
- total_consolidated += 1
145
- if solution_log:
146
- solution_log.update(
147
- result="Solution processed successfully",
148
- heading=f"Solution completed: {txt[:50]}...",
149
- update_progress="none" # Show briefly then disappear
150
- )
151
- else:
152
- if solution_log:
153
- solution_log.update(
154
- result="Solution processing failed",
155
- heading=f"Solution failed: {txt[:50]}...",
156
- update_progress="none" # Show briefly then disappear
157
- )
158
- total_processed += 1
159
-
160
- except Exception as e:
161
- # Log error but continue processing
162
- log_item.update(consolidation_error=str(e))
163
- total_processed += 1
164
-
165
- # Update final results with structured logging
95
+ solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip()
96
log_item.update(
167
- heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated",
168
- solutions=solutions_txt,
169
- result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated",
170
- solutions_processed=total_processed,
171
- solutions_consolidated=total_consolidated,
172
- update_progress="none"
97
+ heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt
98
)
174
- else:
175
- # remove previous solutions too similiar to this one
176
- if set["memory_memorize_replace_threshold"] > 0:
177
- rem += await db.delete_documents_by_query(
178
- query=txt,
179
- threshold=set["memory_memorize_replace_threshold"],
180
- filter=f"area=='{Memory.Area.SOLUTIONS.value}'",
99
+
100
+ # Process solutions with intelligent consolidation
101
+ total_processed = 0
102
+ total_consolidated = 0
103
+ rem = []
104
+
105
+ for solution in solutions:
106
+ # Convert solution to structured text
107
+ if isinstance(solution, dict):
108
+ problem = solution.get('problem', 'Unknown problem')
109
+ solution_text = solution.get('solution', 'Unknown solution')
110
+ txt = f"# Problem\n {problem}\n# Solution\n {solution_text}"
111
+ else:
112
+ # If solution is not a dict, convert it to string
113
+ txt = f"# Solution\n {str(solution)}"
114
+
115
+ if set["memory_memorize_consolidation"]:
116
+ try:
117
+ # Use intelligent consolidation system
118
+ from python.helpers.memory_consolidation import create_memory_consolidator
119
+ consolidator = create_memory_consolidator(
120
+ self.agent,
121
+ similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
122
+ max_similar_memories=6, # Fewer for solutions (more complex)
123
+ max_llm_context_memories=3
124
+ )
125
+
126
+ # Create solution-specific log for detailed tracking
127
+ solution_log = None # too many utility messages, skip log for now
128
+ # solution_log = self.agent.context.log.log(
129
+ # type="util",
130
+ # heading=f"Processing solution: {txt[:50]}...",
131
+ # update_progress="none" # Don't affect status bar
132
+ # )
133
+
134
+ # Process with intelligent consolidation
135
+ result_obj = await consolidator.process_new_memory(
136
+ new_memory=txt,
137
+ area=Memory.Area.SOLUTIONS.value,
138
+ metadata={"area": Memory.Area.SOLUTIONS.value},
139
+ log_item=solution_log
140
+ )
141
+
142
+ # Update the individual log item with completion status but keep it temporary
143
+ if result_obj.get("success"):
144
+ total_consolidated += 1
145
+ if solution_log:
146
+ solution_log.update(
147
+ result="Solution processed successfully",
148
+ heading=f"Solution completed: {txt[:50]}...",
149
+ update_progress="none" # Show briefly then disappear
150
+ )
151
+ else:
152
+ if solution_log:
153
+ solution_log.update(
154
+ result="Solution processing failed",
155
+ heading=f"Solution failed: {txt[:50]}...",
156
+ update_progress="none" # Show briefly then disappear
157
+ )
158
+ total_processed += 1
159
+
160
+ except Exception as e:
161
+ # Log error but continue processing
162
+ log_item.update(consolidation_error=str(e))
163
+ total_processed += 1
164
+
165
+ # Update final results with structured logging
166
+ log_item.update(
167
+ heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated",
168
+ solutions=solutions_txt,
169
+ result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated",
170
+ solutions_processed=total_processed,
171
+ solutions_consolidated=total_consolidated,
172
+ update_progress="none"
173
+ )
174
+ else:
175
+ # remove previous solutions too similiar to this one
176
+ if set["memory_memorize_replace_threshold"] > 0:
177
+ rem += await db.delete_documents_by_query(
178
+ query=txt,
179
+ threshold=set["memory_memorize_replace_threshold"],
180
+ filter=f"area=='{Memory.Area.SOLUTIONS.value}'",
181
+ )
182
+ if rem:
183
+ rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
184
+ log_item.update(replaced=rem_txt)
185
+
186
+ # insert new solution
187
+ await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
188
+
189
+ log_item.update(
190
+ result=f"{len(solutions)} solutions memorized.",
191
+ heading=f"{len(solutions)} solutions memorized.",
192
)
193
if rem:
183
- rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
184
- log_item.update(replaced=rem_txt)
185
-
186
- # insert new solution
187
- await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
188
-
189
- log_item.update(
190
- result=f"{len(solutions)} solutions memorized.",
191
- heading=f"{len(solutions)} solutions memorized.",
192
- )
193
- if rem:
194
- log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.")
194
+ log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.")
195
196
197
- # except Exception as e:
198
- # err = errors.format_error(e)
199
- # self.agent.context.log.log(
200
- # type="error", heading="Memorize solutions extension error:", content=err
201
- # )
197
+ except Exception as e:
198
+ err = errors.format_error(e)
199
+ self.agent.context.log.log(
200
+ type="warning", heading="Memorize solutions extension error", content=err
201
+ )
python/extensions/monologue_end/_90_waiting_for_input_msg.py
+3
@@ -8,3 +8,6 @@ class WaitingForInputMsg(Extension):
8
if self.agent.number == 0:
9
self.agent.context.log.set_initial_progress()
10
11
+ self.agent.context.log.log(
12
+ type="hint", heading="Waiting for input...", content="test content", dumy_kvp1=3, dumy_kvp2="test test"
13
+ )
python/helpers/errors.py
+14
-4
@@ -1,6 +1,7 @@
1
import re
2
import traceback
3
import asyncio
4
+from typing import Literal
5
6
7
def handle_error(e: Exception):
@@ -13,7 +14,7 @@ def error_text(e: Exception):
14
return str(e)
15
16
16
-def format_error(e: Exception, start_entries=20, end_entries=15):
17
+def format_error(e: Exception, start_entries=20, end_entries=15, error_message_position:Literal["top", "bottom", "none"] = "top"):
18
# format traceback from the provided exception instead of the most recent one
19
traceback_text = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
20
# Split the traceback into lines
@@ -50,13 +51,22 @@ def format_error(e: Exception, start_entries=20, end_entries=15):
51
error_message = line
52
break
53
54
+ if error_message and error_message_position in ("top", "bottom", "none"):
55
+ for i in range(len(trimmed_lines) - 1, -1, -1):
56
+ if trimmed_lines[i].strip() == error_message.strip():
57
+ trimmed_lines = trimmed_lines[:i] + trimmed_lines[i + 1 :]
58
+ break
59
+
60
# Combine the trimmed traceback with the error message
61
if not trimmed_lines:
55
- result = error_message
62
+ result = "" if error_message_position == "none" else error_message
63
else:
64
result = "Traceback (most recent call last):\n" + "\n".join(trimmed_lines)
58
- if error_message:
59
- result += f"\n\n{error_message}"
65
+
66
+ if error_message and error_message_position == "top":
67
+ result = f"{error_message}\n\n{result}" if result else error_message
68
+ elif error_message and error_message_position == "bottom":
69
+ result = f"{result}\n\n{error_message}" if result else error_message
70
71
# at least something
72
if not result:
python/helpers/settings.py
+2
-2
@@ -482,7 +482,7 @@ def get_default_settings() -> Settings:
482
return Settings(
483
version=_get_version(),
484
chat_model_provider=get_default_value("chat_model_provider", "openrouter"),
485
- chat_model_name=get_default_value("chat_model_name", "openai/gpt-4.1"),
485
+ chat_model_name=get_default_value("chat_model_name", "openai/gpt-5.2-chat"),
486
chat_model_api_base=get_default_value("chat_model_api_base", ""),
487
chat_model_kwargs=get_default_value("chat_model_kwargs", {"temperature": "0"}),
488
chat_model_ctx_length=get_default_value("chat_model_ctx_length", 100000),
@@ -492,7 +492,7 @@ def get_default_settings() -> Settings:
492
chat_model_rl_input=get_default_value("chat_model_rl_input", 0),
493
chat_model_rl_output=get_default_value("chat_model_rl_output", 0),
494
util_model_provider=get_default_value("util_model_provider", "openrouter"),
495
- util_model_name=get_default_value("util_model_name", "openai/gpt-4.1-mini"),
495
+ util_model_name=get_default_value("util_model_name", "google/gemini-3-flash-preview"),
496
util_model_api_base=get_default_value("util_model_api_base", ""),
497
util_model_ctx_length=get_default_value("util_model_ctx_length", 100000),
498
util_model_ctx_input=get_default_value("util_model_ctx_input", 0.7),
webui/css/messages.css
+18
@@ -286,6 +286,24 @@
286
margin-bottom: var(--spacing-sm);
287
}
288
289
+/* Warning messages styled like process groups */
290
+.message-warning .msg-heading {
291
+ display: flex;
292
+ align-items: center;
293
+ gap: 6px;
294
+ margin-bottom: var(--spacing-xs);
295
+}
296
+
297
+.message-warning .msg-heading h4 {
298
+ display: flex;
299
+ align-items: center;
300
+ font-size: var(--font-size-medium);
301
+ font-weight: 500;
302
+ color: var(--color-warning-text);
303
+ opacity: 0.9;
304
+ margin-bottom: var(--spacing-sm);
305
+}
306
+
307
/* Terminal styling moved to new terminal block above */
308
309
/* Agent and AI Info */
webui/index.css
+4
@@ -24,6 +24,7 @@
24
--color-input-focus-dark: #101010;
25
--color-chat-background-dark: #212121;
26
--color-error-text-dark:#e72323;
27
+ --color-warning-text-dark:#e79c23;
28
--color-table-row-dark: #272727;
29
30
/* Light mode */
@@ -42,6 +43,7 @@
43
--color-input-focus-light: #dadada;
44
--color-chat-background-light: #fafafa;
45
--color-error-text-light:#920000;
46
+ --color-warning-text-light:#936214;
47
--color-table-row-light: #edededf3;
48
49
@@ -62,6 +64,7 @@
64
--color-background-hover: color-mix(in srgb, var(--color-border) 50%, transparent);
65
--color-chat-background: var(--color-chat-background-dark);
66
--color-error-text:var(--color-error-text-dark);
67
+ --color-warning-text:var(--color-warning-text-dark);
68
--color-table-row: var(--color-table-row-dark);
69
70
/* Spacing variables */
@@ -108,6 +111,7 @@
111
--color-background-hover: color-mix(in srgb, var(--color-border) 50%, transparent);
112
--color-chat-background: var(--color-chat-background-light);
113
--color-error-text:var(--color-error-text-light);
114
+ --color-warning-text:var(--color-warning-text-light);
115
--color-table-row: var(--color-table-row-light);
116
}
117
webui/js/messages.js
+121
-91
@@ -15,9 +15,9 @@ import { Scroller } from "./scroller.js";
15
16
// Delay before collapsing previous steps when a new step is added
17
const STEP_COLLAPSE_DELAY = {
18
- "agent": 2000,
19
- "other": 4000, // tools should stay longer as next gen step is placed quickly
20
-}
18
+ agent: 2000,
19
+ other: 4000, // tools should stay longer as next gen step is placed quickly
20
+};
21
// delay collapse when hovering
22
const STEP_COLLAPSE_HOVER_DELAY_MS = 5000;
23
@@ -75,10 +75,14 @@ export function setMessages(messages) {
75
const cutoff = isLargeAppend ? Math.max(0, messages.length - 2) : 0;
76
const massRender = historyEmpty || isLargeAppend;
77
78
- const mainScroller = new Scroller(history, { smooth: !massRender, toleranceRem: 4, reapplyDelayMs: 1000 });
78
+ const mainScroller = new Scroller(history, {
79
+ smooth: !massRender,
80
+ toleranceRem: 4,
81
+ reapplyDelayMs: 1000,
82
+ });
83
+
84
+ const results = [];
85
80
- const results = []
81
-
86
// process messages
87
for (let i = 0; i < messages.length; i++) {
88
_massRender = historyEmpty || (isLargeAppend && i < cutoff);
@@ -90,7 +94,7 @@ export function setMessages(messages) {
94
95
const shouldScroll = historyEmpty || !results[results.length - 1]?.dontScroll;
96
93
- if(shouldScroll) mainScroller.reApplyScroll();
97
+ if (shouldScroll) mainScroller.reApplyScroll();
98
}
99
100
// entrypoint called from poll/WS communication, this is how all messages are rendered and updated
@@ -246,7 +250,11 @@ function drawProcessStep({
250
const isGroupComplete = isProcessGroupComplete(group);
251
252
// Set start timestamp on group when first step is created
249
- if (isNewStep && !group.hasAttribute("data-start-timestamp") && log.timestamp) {
253
+ if (
254
+ isNewStep &&
255
+ !group.hasAttribute("data-start-timestamp") &&
256
+ log.timestamp
257
+ ) {
258
group.setAttribute("data-start-timestamp", String(log.timestamp));
259
}
260
@@ -260,10 +268,13 @@ function drawProcessStep({
268
step.setAttribute("data-log-type", log.type);
269
step.setAttribute("data-step-id", id);
270
step.setAttribute("data-agent-number", log.agentno);
263
-
271
+
272
// set timestamp attribute (convert to milliseconds for duration calculation)
273
if (log.timestamp) {
266
- step.setAttribute("data-timestamp", String(Math.round(log.timestamp * 1000)));
274
+ step.setAttribute(
275
+ "data-timestamp",
276
+ String(Math.round(log.timestamp * 1000)),
277
+ );
278
}
279
280
// apply step classes
@@ -302,8 +313,14 @@ function drawProcessStep({
313
stepsContainer
314
.querySelectorAll(".process-step.expanded")
315
.forEach((expandedStep) => {
305
- const delay = STEP_COLLAPSE_DELAY[expandedStep.getAttribute("data-log-type")] || STEP_COLLAPSE_DELAY.other;
306
- console.log("collapsing", expandedStep.getAttribute("data-log-type"), delay);
316
+ const delay =
317
+ STEP_COLLAPSE_DELAY[expandedStep.getAttribute("data-log-type")] ||
318
+ STEP_COLLAPSE_DELAY.other;
319
+ console.log(
320
+ "collapsing",
321
+ expandedStep.getAttribute("data-log-type"),
322
+ delay,
323
+ );
324
scheduleStepCollapse(expandedStep, delay);
325
});
326
step.classList.add("expanded");
@@ -340,7 +357,8 @@ function drawProcessStep({
357
stepDetail,
358
".process-step-detail-scroll",
359
"div",
343
- "process-step-detail-scroll" );
360
+ "process-step-detail-scroll",
361
+ );
362
363
// set click handlers
364
setupProcessStepHandlers(step, stepHeader);
@@ -367,7 +385,8 @@ function drawProcessStep({
385
386
// auto-scroller of the step detail
387
const detailScroller = new Scroller(stepDetailScroll, {
370
- smooth: !isMassRender(), toleranceRem: 4
388
+ smooth: !isMassRender(),
389
+ toleranceRem: 4,
390
}); // scroller for step detail content
391
392
// update KVPs of the step detail
@@ -489,7 +508,8 @@ export function _drawMessage({
508
509
// Update message classes (preserve collapsible state)
510
const preserve = ["message-collapsible", "expanded", "has-overflow"]
492
- .filter((c) => messageDiv.classList.contains(c)).join(" ");
511
+ .filter((c) => messageDiv.classList.contains(c))
512
+ .join(" ");
513
messageDiv.className = `message ${mainClass} ${messageClasses.join(" ")} ${preserve}`;
514
515
// Handle heading (important for error/rate_limit messages that show context)
@@ -718,7 +738,7 @@ export function drawMessageResponse({
738
}) {
739
// response of subordinate agent - render as process step
740
if (agentno && agentno > 0) {
721
- const title = getStepTitle(heading, kvps, type);
741
+ const title = getStepTitle(heading, content, type);
742
const contentText = String(content ?? "");
743
const actionButtons = contentText.trim()
744
? [
@@ -773,7 +793,7 @@ export function drawMessageResponse({
793
markdown: true,
794
latex: true,
795
mainClass: "message-agent-response",
776
- smoothStream: false ,// smooth render disabled, not reliable yet !isMassRender(), // stream smoothly if not in mass render mode
796
+ smoothStream: false, // smooth render disabled, not reliable yet !isMassRender(), // stream smoothly if not in mass render mode
797
});
798
799
// Collapsible with action buttons
@@ -784,7 +804,12 @@ export function drawMessageResponse({
804
createActionButton("copy", "", () => copyToClipboard(responseText)),
805
].filter(Boolean)
806
: [];
787
- setupCollapsible(messageDiv, ":scope > .step-action-buttons", !isMassRender(), responseActionButtons);
807
+ setupCollapsible(
808
+ messageDiv,
809
+ ":scope > .step-action-buttons",
810
+ !isMassRender(),
811
+ responseActionButtons,
812
+ );
813
814
if (group) updateProcessGroupHeader(group);
815
@@ -798,7 +823,6 @@ export function drawMessageUser({
823
kvps = null,
824
...additional
825
}) {
801
-
826
// end last process group on any user message
827
completeLastProcessGroup();
828
@@ -940,11 +964,10 @@ export function drawMessageTool({
964
agentno = 0,
965
...additional
966
}) {
943
-
967
const tool_name = kvps?._tool_name || "";
968
946
- if(!tool_name){
947
- return drawMessageToolSimple({ ...arguments[0] });
969
+ if (!tool_name) {
970
+ return drawMessageToolSimple({ ...arguments[0] });
971
} else if (kvps._tool_name === "skills_tool") {
972
return drawMessageToolSimple({ ...arguments[0], code: "SKL" });
973
} else if (kvps._tool_name === "vision_load") {
@@ -958,7 +981,6 @@ export function drawMessageTool({
981
} else {
982
return drawMessageToolSimple({ ...arguments[0] });
983
}
961
-
984
}
985
986
export function drawMessageToolSimple({
@@ -1249,7 +1271,6 @@ export function drawMessageUtil({
1271
allowCompletedGroup: true,
1272
});
1273
1252
-
1274
result.dontScroll = !preferencesStore.showUtils;
1275
return result;
1276
}
@@ -1264,7 +1285,7 @@ export function drawMessageHint({
1285
agentno = 0,
1286
...additional
1287
}) {
1267
- const title = getStepTitle(heading, kvps, type);
1288
+ const title = getStepTitle(heading, content, type);
1289
const contentText = String(content ?? "");
1290
const actionButtons = contentText.trim()
1291
? [
@@ -1316,12 +1337,13 @@ export function drawMessageProgress({
1337
1338
export function drawMessageWarning({
1339
id,
1340
+ type,
1341
heading,
1342
content,
1343
kvps = null,
1344
...additional
1345
}) {
1324
- const title = cleanStepTitle(heading || content);
1346
+ const title = getStepTitle(heading, content, type);
1347
let displayKvps = { ...kvps };
1348
const contentText = String(content ?? "");
1349
const actionButtons = contentText.trim()
@@ -1331,39 +1353,46 @@ export function drawMessageWarning({
1353
].filter(Boolean)
1354
: [];
1355
1334
- //TODO: if process group is running, append there instead
1335
- // return drawProcessStep({
1336
- // id,
1337
- // title,
1338
- // code: "WRN",
1339
- // classes: null,
1340
- // kvps: displayKvps,
1341
- // content,
1342
- // // contentClasses: [],
1343
- // log: arguments[0],
1344
- // });
1356
+ //if process group is running, append there
1357
+ const group = getLastProcessGroup(false);
1358
+ if (group) {
1359
+ return drawProcessStep({
1360
+ id,
1361
+ title,
1362
+ code: "WRN",
1363
+ // classes: null,
1364
+ kvps: displayKvps,
1365
+ content,
1366
+ // contentClasses: [],
1367
+ actionButtons,
1368
+ log: arguments[0],
1369
+ });
1370
+ }
1371
+
1372
+ // if no process group is running, draw as standalone
1373
return drawStandaloneMessage({
1374
id,
1347
- heading,
1375
+ title,
1376
content,
1377
position: "mid",
1378
containerClasses: ["ai-container", "center-container"],
1379
mainClass: "message-warning",
1352
- kvps,
1380
+ kvps: displayKvps,
1381
actionButtons,
1382
});
1383
}
1384
1385
export function drawMessageError({
1386
id,
1387
+ type,
1388
heading,
1389
content,
1390
kvps = null,
1391
...additional
1392
}) {
1393
const contentText = String(content ?? "");
1365
- const errorText = kvps?.text || "Error";
1366
- const errorHeading = errorText ? `Error - ${errorText}` : "Error";
1394
+ let title = getStepTitle(heading, content, type);
1395
+ let displayKvps = { ...kvps };
1396
const actionButtons = [
1397
createActionButton("detail", "", () =>
1398
stepDetailStore.showStepDetail(
@@ -1377,11 +1406,12 @@ export function drawMessageError({
1406
1407
return drawStandaloneMessage({
1408
id,
1380
- heading: errorHeading,
1381
- content,
1409
+ heading: title,
1410
+ content: contentText,
1411
position: "mid",
1412
containerClasses: ["ai-container", "center-container"],
1413
mainClass: "message-error",
1414
+ kvps: displayKvps,
1415
actionButtons,
1416
});
1417
}
@@ -1542,7 +1572,6 @@ function convertFilePaths(str) {
1572
return str.replace(/file:\/\//g, "/download_work_dir_file?path=");
1573
}
1574
1545
-
1575
function escapeHTML(str) {
1576
const escapeChars = {
1577
"&": "&",
@@ -1743,7 +1772,10 @@ function getNestedContainer(parentStep) {
1772
* Schedule a step to collapse after a delay
1773
* Automatically handles cancellation on click and reset on hover
1774
*/
1746
-function scheduleStepCollapse(stepElement, delayMs=STEP_COLLAPSE_DELAY.other) {
1775
+function scheduleStepCollapse(
1776
+ stepElement,
1777
+ delayMs = STEP_COLLAPSE_DELAY.other,
1778
+) {
1779
// skip if any existing timeout for this step
1780
if (stepElement.hasAttribute("data-collapse-timeout-id")) return;
1781
// skip already collapsed steps
@@ -1830,33 +1862,14 @@ function findParentDelegationStep(group, agentno) {
1862
/**
1863
* Get a concise title for a process step
1864
*/
1833
-function getStepTitle(heading, kvps, type) {
1865
+function getStepTitle(heading, content, type) {
1866
// Try to get a meaningful title from heading or kvps
1867
if (heading && heading.trim()) {
1836
- return cleanStepTitle(heading, 100);
1837
- }
1838
-
1839
- // For warnings/errors without heading, use content preview as title
1840
- if (type === "warning" || type === "error") {
1841
- // We'll show full content in detail, so just use type as title
1842
- return type === "warning" ? "Warning" : "Error";
1868
+ return cleanStepTitle(heading, 60);
1869
}
1870
1845
- if (kvps) {
1846
- // Try common fields for title
1847
- if (kvps.tool_name) {
1848
- const headline = kvps.headline ? cleanStepTitle(kvps.headline, 60) : "";
1849
- return `${kvps.tool_name}${headline ? ": " + headline : ""}`;
1850
- }
1851
- if (kvps.headline) {
1852
- return cleanStepTitle(kvps.headline, 100);
1853
- }
1854
- if (kvps.query) {
1855
- return truncateText(kvps.query, 100);
1856
- }
1857
- if (kvps.thoughts) {
1858
- return truncateText(String(kvps.thoughts), 100);
1859
- }
1871
+ if (content && content.trim()) {
1872
+ return cleanStepTitle(content, 60);
1873
}
1874
1875
// Fallback: capitalize type (backend is source of truth)
@@ -1898,14 +1911,10 @@ export function convertIcons(html, classes = "") {
1911
*/
1912
function cleanStepTitle(text, maxLength = 100) {
1913
if (!text) return "";
1901
- let cleaned = String(text);
1902
-
1903
- // Remove icon:// patterns (e.g., "icon://network_intelligence" or "icon://network_intelligence[Tooltip]")
1904
- cleaned = cleaned.replace(/icon:\/\/[a-zA-Z0-9_]+(\[(?:\\.|[^\]])*\])?\s*/g, "");
1905
-
1906
- // Trim whitespace
1907
- cleaned = cleaned.trim();
1908
-
1914
+ let cleaned = String(text)
1915
+ .replace(/icon:\/\/[a-zA-Z0-9_]+(\[(?:\\.|[^\]])*\])?\s*/g, "")
1916
+ .replace(/\s+/g, " ")
1917
+ .trim();
1918
return truncateText(cleaned, maxLength);
1919
}
1920
@@ -1959,12 +1968,16 @@ function updateProcessGroupHeader(group) {
1968
1969
// Update step count in metrics - All GEN steps from all agents per process group
1970
const stepMetricContainerEl = metricsEl?.querySelector(".metric-steps");
1962
- const stepsMetricValEl = stepMetricContainerEl?.querySelector(".metric-value");
1971
+ const stepsMetricValEl =
1972
+ stepMetricContainerEl?.querySelector(".metric-value");
1973
if (stepsMetricValEl) {
1964
- let genSteps = group.querySelectorAll('.process-step[data-log-type="agent"]').length;
1974
+ let genSteps = group.querySelectorAll(
1975
+ '.process-step[data-log-type="agent"]',
1976
+ ).length;
1977
genSteps -= 1; // don't count response as step
1978
stepsMetricValEl.textContent = genSteps.toString();
1967
- if (genSteps <= 0) stepMetricContainerEl.classList.add("display-none"); // hide when no steps
1979
+ if (genSteps <= 0)
1980
+ stepMetricContainerEl.classList.add("display-none"); // hide when no steps
1981
else stepMetricContainerEl.classList.remove("display-none");
1982
}
1983
@@ -1982,8 +1995,8 @@ function updateProcessGroupHeader(group) {
1995
dateStyle: "medium",
1996
timeStyle: "short",
1997
});
1985
- timeMetricContainerEl.title = timeMetricContainerEl.dataset.bsOriginalTitle =
1986
- fullDateTime;
1998
+ timeMetricContainerEl.title =
1999
+ timeMetricContainerEl.dataset.bsOriginalTitle = fullDateTime;
2000
}
2001
}
2002
@@ -2003,8 +2016,10 @@ function updateProcessGroupHeader(group) {
2016
lastTimestampMs > 0 &&
2017
formatDuration(Math.max(0, lastTimestampMs - firstTimestampMs));
2018
2006
- const durationMetricContainerEl = metricsEl?.querySelector(".metric-duration");
2007
- const durationMetricValEl = durationMetricContainerEl?.querySelector(".metric-value");
2019
+ const durationMetricContainerEl =
2020
+ metricsEl?.querySelector(".metric-duration");
2021
+ const durationMetricValEl =
2022
+ durationMetricContainerEl?.querySelector(".metric-value");
2023
if (durationMetricContainerEl && durationMetricValEl && durationText) {
2024
durationMetricValEl.textContent = durationText;
2025
durationMetricContainerEl.classList.remove("display-none");
@@ -2084,11 +2099,21 @@ function ensureChild(parent, selector, tagName, ...classNames) {
2099
}
2100
2101
// Setup collapsible message with expand button and action buttons
2087
-function setupCollapsible(messageDiv, containerSelector, initialExpanded, actionButtons = []) {
2102
+function setupCollapsible(
2103
+ messageDiv,
2104
+ containerSelector,
2105
+ initialExpanded,
2106
+ actionButtons = [],
2107
+) {
2108
messageDiv.classList.add("message-collapsible");
2109
messageDiv.classList.toggle("expanded", initialExpanded);
2110
2091
- const container = ensureChild(messageDiv, containerSelector, "div", "step-action-buttons");
2111
+ const container = ensureChild(
2112
+ messageDiv,
2113
+ containerSelector,
2114
+ "div",
2115
+ "step-action-buttons",
2116
+ );
2117
container.textContent = "";
2118
2119
const btn = ensureChild(container, ".expand-btn", "button", "expand-btn");
@@ -2102,8 +2127,8 @@ function setupCollapsible(messageDiv, containerSelector, initialExpanded, action
2127
btn.onclick = () => {
2128
messageDiv.classList.toggle("expanded");
2129
syncBtn();
2105
- messageDiv.classList.contains("expanded")
2106
- || (messageDiv.querySelector(".message-body").scrollTop = 0);
2130
+ messageDiv.classList.contains("expanded") ||
2131
+ (messageDiv.querySelector(".message-body").scrollTop = 0);
2132
};
2133
2134
actionButtons.filter(Boolean).forEach((b) => container.appendChild(b));
@@ -2111,11 +2136,16 @@ function setupCollapsible(messageDiv, containerSelector, initialExpanded, action
2136
// Detect overflow after render
2137
requestAnimationFrame(() => {
2138
const body = messageDiv.querySelector(".message-body");
2114
- const fontSize = parseFloat(getComputedStyle(body || document.documentElement).fontSize || "16");
2139
+ const fontSize = parseFloat(
2140
+ getComputedStyle(body || document.documentElement).fontSize || "16",
2141
+ );
2142
const maxHeight = messageDiv.classList.contains("expanded")
2143
? fontSize * 15
2117
- : (body?.clientHeight || 0);
2118
- messageDiv.classList.toggle("has-overflow", (body?.scrollHeight || 0) > maxHeight);
2144
+ : body?.clientHeight || 0;
2145
+ messageDiv.classList.toggle(
2146
+ "has-overflow",
2147
+ (body?.scrollHeight || 0) > maxHeight,
2148
+ );
2149
});
2150
}
2151
@@ -2129,7 +2159,7 @@ function isMassRender() {
2159
function smoothRender(element, newContent, delay = 350) {
2160
// skip on mass render
2161
if (isMassRender()) {
2132
- element.innerHTML = newContent;
2162
+ element.innerHTML = newContent;
2163
return;
2164
}
2165