@@ -130,9 +130,20 @@ fn ThinkingView(content: String, is_streaming: bool) -> impl IntoView {
130130 >
131131 <summary class="flex items-center justify-between px-4 py-2.5 cursor-pointer font-medium text-xs text-amber-600 dark:text-amber-400 bg-amber-50/40 dark:bg-amber-950/10 hover:bg-amber-100/50 dark:hover:bg-amber-950/20 transition-colors select-none" >
132132 <div class="flex items-center gap-2" >
133- <span class=format!( "thinking-icon text-sm {}" , if is_streaming { "animate-spin" } else { "" } ) >
134- "\u{1F9E0} "
135- </span>
133+ { if is_streaming {
134+ view! {
135+ <svg class="w-3.5 h-3.5 flex-shrink-0 animate-spin text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" >
136+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" ></circle>
137+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" ></path>
138+ </svg>
139+ } . into_any( )
140+ } else {
141+ view! {
142+ <span class="thinking-icon text-sm select-none" >
143+ "\u{1F9E0} "
144+ </span>
145+ } . into_any( )
146+ } }
136147 <span class="tracking-wide uppercase font-semibold text-[10px]" >
137148 { if is_streaming { "Thinking process..." } else { "Thought process" } }
138149 </span>
@@ -201,6 +212,49 @@ fn ToolCallView(
201212 . unwrap_or ( & name)
202213 . to_string ( ) ;
203214
215+ let tool_icon = match name. as_str ( ) {
216+ "LIST_DIR" => view ! {
217+ <svg class="w-3.5 h-3.5 flex-shrink-0 opacity-60" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" >
218+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
219+ </svg>
220+ } . into_any ( ) ,
221+ "FIND_FILE" | "SEARCH_DIR" => view ! {
222+ <svg class="w-3.5 h-3.5 flex-shrink-0 opacity-60" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" >
223+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
224+ </svg>
225+ } . into_any ( ) ,
226+ "RUN_COMMAND" => view ! {
227+ <svg class="w-3.5 h-3.5 flex-shrink-0 opacity-60" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" >
228+ <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
229+ </svg>
230+ } . into_any ( ) ,
231+ "VIEW_FILE" => view ! {
232+ <svg class="w-3.5 h-3.5 flex-shrink-0 opacity-60" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" >
233+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
234+ </svg>
235+ } . into_any ( ) ,
236+ "EDIT_FILE" | "CREATE_FILE" => view ! {
237+ <svg class="w-3.5 h-3.5 flex-shrink-0 opacity-60" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" >
238+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />
239+ </svg>
240+ } . into_any ( ) ,
241+ "START_SUBAGENT" => view ! {
242+ <svg class="w-3.5 h-3.5 flex-shrink-0 opacity-60" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" >
243+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 3v1.5M12 3v1.5m3.75-1.5v1.5M19.5 8.25h-1.5M19.5 12h-1.5m1.5 3.75h-1.5m-14.25-3.75h-1.5m1.5-3.75h-1.5m1.5 7.5h-1.5m3 3V21M12 19.5V21m3.75-1.5V21m-9-15h10.5a1.5 1.5 0 011.5 1.5v10.5a1.5 1.5 0 01-1.5 1.5H6.75a1.5 1.5 0 01-1.5-1.5V6.75a1.5 1.5 0 011.5-1.5zm2.25 4.5h4.5v4.5h-4.5v-4.5z" />
244+ </svg>
245+ } . into_any ( ) ,
246+ "GENERATE_IMAGE" => view ! {
247+ <svg class="w-3.5 h-3.5 flex-shrink-0 opacity-60" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" >
248+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
249+ </svg>
250+ } . into_any ( ) ,
251+ _ => view ! {
252+ <svg class="w-3.5 h-3.5 flex-shrink-0 opacity-60" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" >
253+ <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75a4.5 4.5 0 01-4.884 4.484c-1.076-.091-2.264.071-2.95.904l-7.152 8.684a2.548 2.548 0 11-3.586-3.586l8.684-7.152c.833-.686.995-1.874.904-2.95a4.5 4.5 0 016.336-4.486l-3.276 3.276a3.004 3.004 0 002.25 2.25l3.276-3.276c.256.565.398 1.192.398 1.852z" />
254+ </svg>
255+ } . into_any ( ) ,
256+ } ;
257+
204258 // `<details>` is open while the tool is actively running so the user can
205259 // watch the arguments in real time; it collapses automatically when done.
206260 let is_open = matches ! ( status, ToolCallStatus :: Running ) ; // Theme tokens per status
@@ -286,8 +340,10 @@ fn ToolCallView(
286340 "flex items-center justify-between px-3 py-2 cursor-pointer text-xs select-none transition-colors {}" ,
287341 summary_cls
288342 ) >
289- // Left: icon + human-readable title + raw tool chip + subtitle
343+ // Left: wrench icon + status icon + human-readable title + raw tool chip + subtitle
290344 <div class="flex items-center gap-2 flex-1 min-w-0" >
345+ // Dynamic tool call icon
346+ { tool_icon}
291347 { status_icon}
292348 // Primary: intent label ("Change Directory") not raw name
293349 <span class="font-semibold truncate" >{ display_title} </span>
@@ -2563,18 +2619,15 @@ fn ChatPage() -> impl IntoView {
25632619 let stream_completed_nat = stream_completed. clone ( ) ;
25642620 let do_finalize_nat = do_finalize. clone ( ) ;
25652621 let native_err_closure = Closure :: < dyn FnMut ( _) > :: new ( move |_event : web_sys:: Event | {
2566- // Only show an error if the stream was NOT completed intentionally.
2622+ // Finalize if the stream wasn't already completed.
2623+ // We intentionally do NOT push an Error block here — the native
2624+ // onerror fires on transport-level disconnects AND when we call
2625+ // es.close() ourselves after a normal idle/done sequence. Any
2626+ // server-sent errors already arrive via the named "error" SSE
2627+ // event handler above. Pushing "Connection to agent lost" from
2628+ // here causes false positives after every normal completion.
25672629 if !stream_completed_nat. get ( ) {
2568- // Finalise first: collapses thinking block, saves accumulated
2569- // blocks to KV so we don't lose partial responses on error.
25702630 do_finalize_nat ( ) ;
2571-
2572- let err_id = next_id ( ) ;
2573- set_blocks. update ( |bs| bs. push ( MessageBlock :: Error {
2574- id : err_id,
2575- message : "Connection to agent lost" . to_string ( ) ,
2576- http_code : None ,
2577- } ) ) ;
25782631 }
25792632 active_es_native_err. update_value ( |opt_es| {
25802633 if let Some ( es) = opt_es. take ( ) {
@@ -3165,7 +3218,22 @@ fn ChatPage() -> impl IntoView {
31653218 // and as the step.content for the ToolCall — we use it as the
31663219 // card title and suppress the redundant standalone bubble).
31673220 { move || {
3168- let all_blocks = blocks. get( ) ;
3221+ let mut all_blocks = blocks. get( ) ;
3222+ // Reorder: ensure Thinking blocks appear before adjacent
3223+ // AssistantMessage blocks. The server may emit text tokens
3224+ // before thinking deltas for the same step, causing
3225+ // AssistantMessage to be pushed before Thinking in the vec.
3226+ {
3227+ let mut i = 1 ;
3228+ while i < all_blocks. len( ) {
3229+ if matches!( all_blocks[ i] , MessageBlock :: Thinking { .. } )
3230+ && matches!( all_blocks[ i - 1 ] , MessageBlock :: AssistantMessage { .. } )
3231+ {
3232+ all_blocks. swap( i - 1 , i) ;
3233+ }
3234+ i += 1 ;
3235+ }
3236+ }
31693237 // Collect all non-empty short labels from ToolCall blocks
31703238 let tool_labels: std:: collections:: HashSet <String > = all_blocks. iter( )
31713239 . filter_map( |b| match b {
0 commit comments