From 2bccb11e53b2d5ad6a01c87a1f975c9c3cc3a7d5 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Fri, 26 Dec 2025 19:12:02 +0100 Subject: [PATCH] feat(core): Introduce AppState and integrate with Engine Loop --- crates/app/cli/src/engine.rs | 65 +++++++++++++++++++++++++++++++++--- crates/app/cli/src/main.rs | 7 +++- crates/app/cli/src/state.rs | 42 +++++++++++++++++++++++ 3 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 crates/app/cli/src/state.rs diff --git a/crates/app/cli/src/engine.rs b/crates/app/cli/src/engine.rs index f9370be..dcb970d 100644 --- a/crates/app/cli/src/engine.rs +++ b/crates/app/cli/src/engine.rs @@ -1,5 +1,6 @@ use crate::messages::{Message, UserAction, AgentResponse}; -use tokio::sync::mpsc; +use crate::state::AppState; +use tokio::sync::{mpsc, Mutex}; use std::sync::Arc; use llm_core::{LlmProvider, ChatMessage, ChatOptions}; use futures::StreamExt; @@ -9,21 +10,30 @@ pub async fn run_engine_loop( mut rx: mpsc::Receiver, tx_ui: mpsc::Sender, client: Arc, + state: Arc>, ) { while let Some(msg) = rx.recv().await { match msg { Message::UserAction(UserAction::Input(text)) => { - // TODO: Maintain conversation history (AppState) - next task - let messages = vec![ChatMessage::user(text)]; + // Update history with user message + let messages = { + let mut guard = state.lock().await; + guard.add_message(ChatMessage::user(text.clone())); + guard.messages.clone() + }; + // Use default options for now let options = ChatOptions::default(); match client.chat_stream(&messages, &options, None).await { Ok(mut stream) => { + let mut full_response = String::new(); + while let Some(result) = stream.next().await { match result { Ok(chunk) => { if let Some(content) = chunk.content { + full_response.push_str(&content); if let Err(e) = tx_ui.send(Message::AgentResponse(AgentResponse::Token(content))).await { eprintln!("Failed to send token to UI: {}", e); break; @@ -35,6 +45,13 @@ pub async fn run_engine_loop( } } } + + // Add assistant response to history + { + let mut guard = state.lock().await; + guard.add_message(ChatMessage::assistant(full_response)); + } + let _ = tx_ui.send(Message::AgentResponse(AgentResponse::Complete)).await; } Err(e) => { @@ -82,10 +99,11 @@ mod tests { let (tx_out, mut rx_out) = mpsc::channel(10); let client = Arc::new(MockProvider); + let state = Arc::new(Mutex::new(AppState::new())); // Spawn the engine loop tokio::spawn(async move { - run_engine_loop(rx_in, tx_out, client).await; + run_engine_loop(rx_in, tx_out, client, state).await; }); // Send a message @@ -110,4 +128,41 @@ mod tests { panic!("Expected Complete"); } } -} \ No newline at end of file + + #[tokio::test] + async fn test_engine_state_update() { + let (tx_in, rx_in) = mpsc::channel(1); + let (tx_out, mut rx_out) = mpsc::channel(10); + + let client = Arc::new(MockProvider); + let state = Arc::new(Mutex::new(AppState::new())); + let state_clone = state.clone(); + + // Spawn the engine loop + tokio::spawn(async move { + run_engine_loop(rx_in, tx_out, client, state_clone).await; + }); + + // Send a message + tx_in.send(Message::UserAction(UserAction::Input("Hi".to_string()))).await.unwrap(); + + // Wait for completion + while let Some(msg) = rx_out.recv().await { + if let Message::AgentResponse(AgentResponse::Complete) = msg { + break; + } + } + + // Verify state + let guard = state.lock().await; + assert_eq!(guard.messages.len(), 2); // User + Assistant + match &guard.messages[0].role { + llm_core::Role::User => {}, + _ => panic!("First message should be User"), + } + match &guard.messages[1].role { + llm_core::Role::Assistant => {}, + _ => panic!("Second message should be Assistant"), + } + } +} diff --git a/crates/app/cli/src/main.rs b/crates/app/cli/src/main.rs index 4733cba..9a19883 100644 --- a/crates/app/cli/src/main.rs +++ b/crates/app/cli/src/main.rs @@ -1,6 +1,7 @@ mod commands; mod messages; mod engine; +mod state; use clap::{Parser, ValueEnum}; use color_eyre::eyre::{Result, eyre}; @@ -763,11 +764,15 @@ async fn main() -> Result<()> { // Keep tx for future use (will be passed to UI/REPL) let _tx = tx.clone(); + // Create shared state + let state = Arc::new(tokio::sync::Mutex::new(crate::state::AppState::new())); + // Spawn the Engine Loop let client_clone = client.clone(); let tx_clone = tx.clone(); + let state_clone = state.clone(); tokio::spawn(async move { - engine::run_engine_loop(rx, tx_clone, client_clone).await; + engine::run_engine_loop(rx, tx_clone, client_clone, state_clone).await; }); // Check if interactive mode (no prompt provided) diff --git a/crates/app/cli/src/state.rs b/crates/app/cli/src/state.rs new file mode 100644 index 0000000..d5cce80 --- /dev/null +++ b/crates/app/cli/src/state.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; +use tokio::sync::Mutex; +use llm_core::ChatMessage; + +/// Shared application state +#[derive(Debug, Default)] +pub struct AppState { + pub messages: Vec, + pub running: bool, +} + +impl AppState { + pub fn new() -> Self { + Self { + messages: Vec::new(), + running: true, + } + } + + pub fn add_message(&mut self, message: ChatMessage) { + self.messages.push(message); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_app_state_sharing() { + let state = Arc::new(Mutex::new(AppState::new())); + + let state_clone = state.clone(); + tokio::spawn(async move { + let mut guard = state_clone.lock().await; + guard.add_message(ChatMessage::user("Test")); + }).await.unwrap(); + + let guard = state.lock().await; + assert_eq!(guard.messages.len(), 1); + } +}