mind: shared MindState for pending input

Add MindState behind Arc<Mutex<>> for state shared between Mind
and UI. Pending user input goes through shared state instead of
MindMessage::UserInput — UI pushes, Mind consumes.

Mind checks for pending input after every event (message received,
turn completed, DMN tick). User input is prioritized over DMN ticks.

This enables the UI to display/edit/cancel queued messages, and
removes the last MindMessage variant that carried data.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-05 02:52:56 -04:00
parent 05d6bbc912
commit 792e9440af
2 changed files with 61 additions and 49 deletions

View file

@ -19,7 +19,6 @@ use crate::user::ui_channel::{self, UiMessage};
/// Messages from the UI to the Mind.
pub enum MindMessage {
UserInput(String),
Hotkey(HotkeyAction),
DmnSleep,
DmnWake,
@ -60,8 +59,8 @@ fn send_help(ui_tx: &ui_channel::UiSender) {
async fn cmd_retry(
agent: &Arc<Mutex<Agent>>,
shared_mind: &crate::mind::SharedMindState,
ui_tx: &ui_channel::UiSender,
mind_tx: &tokio::sync::mpsc::UnboundedSender<MindMessage>,
) {
let mut agent_guard = agent.lock().await;
let entries = agent_guard.entries_mut();
@ -78,7 +77,7 @@ async fn cmd_retry(
Some(text) => {
let preview_len = text.len().min(60);
let _ = ui_tx.send(UiMessage::Info(format!("(retrying: {}...)", &text[..preview_len])));
let _ = mind_tx.send(MindMessage::UserInput(text));
shared_mind.lock().unwrap().input.push(text);
}
None => {
let _ = ui_tx.send(UiMessage::Info("(nothing to retry)".into()));
@ -177,6 +176,8 @@ pub async fn cmd_switch_model(
pub async fn run(
mut app: tui::App,
agent: Arc<Mutex<Agent>>,
shared_mind: crate::mind::SharedMindState,
turn_watch: tokio::sync::watch::Receiver<bool>,
mind_tx: tokio::sync::mpsc::UnboundedSender<MindMessage>,
ui_tx: ui_channel::UiSender,
@ -295,13 +296,12 @@ pub async fn run(
"/score" => { let _ = mind_tx.send(MindMessage::Score); }
"/retry" => {
let agent = agent.clone();
let sm = shared_mind.clone();
let ui_tx = ui_tx.clone();
let mind_tx = mind_tx.clone();
let mut tw = turn_watch.clone();
tokio::spawn(async move {
// Wait for any in-progress turn to complete
let _ = tw.wait_for(|&active| !active).await;
cmd_retry(&agent, &ui_tx, &mind_tx).await;
cmd_retry(&agent, &sm, &ui_tx).await;
});
}
cmd if cmd.starts_with("/model ") => {
@ -316,7 +316,9 @@ pub async fn run(
});
}
}
_ => { let _ = mind_tx.send(MindMessage::UserInput(input)); }
_ => {
shared_mind.lock().unwrap().input.push(input);
}
}
}