feat(tui): implement streaming support for LLM responses and update transcript handling

This commit is contained in:
asepharyana
2026-07-20 14:51:16 +07:00
parent 5a373d1031
commit cac6626586
3 changed files with 45 additions and 8 deletions
+14
View File
@@ -158,6 +158,20 @@ pub fn apply_action(state: &mut crate::state::AppStateRest, action: Action) {
zesdex_infrastructure::TurnEvent::Error(msg) => { zesdex_infrastructure::TurnEvent::Error(msg) => {
state.toast_error(msg); state.toast_error(msg);
} }
zesdex_infrastructure::TurnEvent::StreamStart => {
state.push_transcript(crate::state::ChatMessageDisplay::new(
zesdex_domain::core::Role::Assistant,
String::new(),
));
}
zesdex_infrastructure::TurnEvent::StreamToken(text) => {
state.append_to_last_transcript(&text);
}
zesdex_infrastructure::TurnEvent::StreamDone(_msg) => {
// The final message is already accumulated in the transcript.
// We might want to trigger a save or something here, but for UI, we just mark dirty.
state.dirty = true;
}
zesdex_infrastructure::TurnEvent::Compacted(msgs) => { zesdex_infrastructure::TurnEvent::Compacted(msgs) => {
if let Some(ref mut rt) = state.session_runtime { if let Some(ref mut rt) = state.session_runtime {
rt.messages = msgs; rt.messages = msgs;
+11
View File
@@ -1008,6 +1008,17 @@ impl AppStateRest {
self.dirty = true; self.dirty = true;
} }
/// Append text to the last assistant message in the transcript, if one exists.
pub fn append_to_last_transcript(&mut self, text: &str) {
if let Some(msg) = self.transcript_cache.messages.back_mut() {
if msg.role == zesdex_domain::core::Role::Assistant {
msg.content.push_str(text);
self.transcript_cache.dirty = true;
self.dirty = true;
}
}
}
/// Mark the app state as dirty, triggering a TUI re-render. /// Mark the app state as dirty, triggering a TUI re-render.
pub fn mark_dirty(&mut self) { pub fn mark_dirty(&mut self) {
self.dirty = true; self.dirty = true;
+20 -8
View File
@@ -128,13 +128,27 @@ fn run_turn(
debug!("agent turn iteration {iteration}"); debug!("agent turn iteration {iteration}");
// Blocking LLM call (reqwest::blocking::Client is sync) // Stream the LLM response
let result = client.chat_with_tools_non_streaming( push_event(turn_events, TurnEvent::StreamStart);
let result = client.chat_with_tools_streaming(
messages, messages,
Some(defs.clone()), Some(defs.clone()),
Some(4096),
Some(0.7), Some(0.7),
None, // no atomic abort flag for the sync API Some(4096),
|event| {
if abort.load(Ordering::SeqCst) {
return false;
}
match event {
zesdex_domain::core::StreamEvent::Token(s) | zesdex_domain::core::StreamEvent::Reasoning(s) => {
push_event(turn_events, TurnEvent::StreamToken(s.clone()));
}
_ => {}
}
true
},
Some(&abort),
); );
match result { match result {
@@ -142,6 +156,8 @@ fn run_turn(
let content = assistant_msg.content.clone().unwrap_or_default(); let content = assistant_msg.content.clone().unwrap_or_default();
let tool_calls = assistant_msg.tool_calls.clone().unwrap_or_default(); let tool_calls = assistant_msg.tool_calls.clone().unwrap_or_default();
push_event(turn_events, TurnEvent::StreamDone(assistant_msg.clone()));
if let Some((tokens_in, tokens_out)) = usage { if let Some((tokens_in, tokens_out)) = usage {
push_event(turn_events, TurnEvent::Usage { push_event(turn_events, TurnEvent::Usage {
tokens_in, tokens_in,
@@ -149,10 +165,6 @@ fn run_turn(
}); });
} }
if !content.is_empty() {
push_event(turn_events, TurnEvent::AssistantMessage(assistant_msg.clone()));
}
if tool_calls.is_empty() { if tool_calls.is_empty() {
messages.push(ChatMessage::assistant(Some(content))); messages.push(ChatMessage::assistant(Some(content)));
break; break;