1212
1313from uipath .dev .models import ChatMessage , ExecutionRun
1414
15+ # Tunables for streaming performance
16+ STREAM_MIN_INTERVAL = 0.08 # seconds between updates while streaming
17+ STREAM_MIN_DELTA_CHARS = 8 # min new chars before we bother updating
18+
1519
1620class Prompt (Markdown ):
1721 """User prompt message bubble."""
@@ -36,12 +40,14 @@ class ChatPanel(Container):
3640
3741 _chat_widgets : dict [str , Markdown ]
3842 _last_update_time : dict [str , float ]
43+ _last_content : dict [str , str ]
3944
4045 def __init__ (self , ** kwargs ):
4146 """Initialize the chat panel."""
4247 super ().__init__ (** kwargs )
4348 self ._chat_widgets = {}
4449 self ._last_update_time = {}
50+ self ._last_content = {}
4551
4652 def compose (self ) -> ComposeResult :
4753 """Compose the UI layout."""
@@ -52,19 +58,21 @@ def compose(self) -> ComposeResult:
5258 id = "chat-input" ,
5359 )
5460
55- def update_messages (self , run : ExecutionRun ) -> None :
61+ def refresh_messages (self , run : ExecutionRun ) -> None :
5662 """Update the chat panel with messages from the given execution run."""
57- chat_view = self .query_one ("#chat-view" )
63+ chat_view = self .query_one ("#chat-view" , VerticalScroll )
5864 chat_view .remove_children ()
5965 self ._chat_widgets .clear ()
6066 self ._last_update_time .clear ()
67+ self ._last_content .clear ()
6168
6269 for chat_msg in run .messages :
6370 self .add_chat_message (
6471 ChatMessage (message = chat_msg , event = None , run_id = run .id ),
6572 auto_scroll = False ,
6673 )
6774
75+ # For a fresh run, always show the latest messages
6876 chat_view .scroll_end (animate = False )
6977
7078 def add_chat_message (
@@ -73,10 +81,11 @@ def add_chat_message(
7381 auto_scroll : bool = True ,
7482 ) -> None :
7583 """Add or update a chat message bubble."""
76- chat_view = self .query_one ("#chat-view" )
84+ chat_view = self .query_one ("#chat-view" , VerticalScroll )
7785
78- message = chat_msg . message
86+ should_autoscroll = auto_scroll and not chat_view . is_vertical_scrollbar_grabbed
7987
88+ message = chat_msg .message
8089 if message is None :
8190 return
8291
@@ -114,27 +123,67 @@ def add_chat_message(
114123
115124 content = "\n \n " .join (content_lines )
116125
126+ prev_content = self ._last_content .get (message .message_id )
127+ if prev_content is not None and content == prev_content :
128+ # We already rendered this exact content, no need to touch the UI.
129+ return
130+
117131 existing = self ._chat_widgets .get (message .message_id )
118132 now = time .monotonic ()
119133 last_update = self ._last_update_time .get (message .message_id , 0.0 )
134+ prev_content_len = len (prev_content ) if prev_content is not None else 0
135+ delta_len = len (content ) - prev_content_len
120136
121- if existing :
137+ def should_update () -> bool :
122138 event = chat_msg .event
123- should_update = (
139+ finished = (
124140 event
125141 and event .exchange
126142 and event .exchange .message
127143 and event .exchange .message .end is not None
128144 )
129- if should_update or now - last_update > 0.15 :
145+
146+ if finished :
147+ # Always paint the final state immediately.
148+ return True
149+
150+ # Throttle streaming: require both some time and a minimum delta size.
151+ if now - last_update < STREAM_MIN_INTERVAL :
152+ return False
153+
154+ if delta_len < STREAM_MIN_DELTA_CHARS :
155+ return False
156+
157+ return True
158+
159+ if existing :
160+ if not should_update ():
161+ return
162+
163+ # Fast path: message is growing by appending new text.
164+ if (
165+ isinstance (existing , Markdown )
166+ and prev_content is not None
167+ and content .startswith (prev_content )
168+ ):
169+ delta = content [len (prev_content ) :]
170+ if delta :
171+ # Streaming update: only append the new portion.
172+ existing .append (delta )
173+ else :
174+ # Fallback for non-monotonic changes: full update.
130175 existing .update (content )
131- self . _last_update_time [ message . message_id ] = now
132- if auto_scroll :
133- chat_view . scroll_end ( animate = False )
176+
177+ self . _last_content [ message . message_id ] = content
178+ self . _last_update_time [ message . message_id ] = now
134179 else :
180+ # First time we see this message: create a new widget.
135181 widget_instance = widget_cls (content )
136182 chat_view .mount (widget_instance )
137183 self ._chat_widgets [message .message_id ] = widget_instance
138184 self ._last_update_time [message .message_id ] = now
139- if auto_scroll :
140- chat_view .scroll_end (animate = False )
185+ self ._last_content [message .message_id ] = content
186+
187+ # Only auto-scroll if we were at the bottom *before* the update
188+ if should_autoscroll :
189+ chat_view .scroll_end (animate = False )
0 commit comments