mind: double-buffer MindState for UI diffing
UI event loop clones MindState on each render tick, diffs against the previous copy, and generates status updates from changes. Mind no longer sends UiMessage::StatusUpdate — state changes are detected automatically by the UI. Removes update_status from both Mind and event_loop. DMN state changes, turn tracking, scoring status all flow through the diff. Zero UiMessage sends from Mind's run loop for state changes. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
54cd3783eb
commit
07ca136c14
3 changed files with 24 additions and 33 deletions
|
|
@ -22,7 +22,7 @@ use std::path::PathBuf;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
/// DMN state machine.
|
/// DMN state machine.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
/// Responding to user input. Short interval — stay engaged.
|
/// Responding to user input. Short interval — stay engaged.
|
||||||
Engaged,
|
Engaged,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ fn compaction_threshold(app: &AppConfig) -> u32 {
|
||||||
|
|
||||||
|
|
||||||
/// Shared state between Mind and UI.
|
/// Shared state between Mind and UI.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct MindState {
|
pub struct MindState {
|
||||||
/// Pending user input — UI pushes, Mind consumes after turn completes.
|
/// Pending user input — UI pushes, Mind consumes after turn completes.
|
||||||
pub input: Vec<String>,
|
pub input: Vec<String>,
|
||||||
|
|
@ -319,10 +320,7 @@ impl Mind {
|
||||||
Some(msg) = input_rx.recv() => {
|
Some(msg) = input_rx.recv() => {
|
||||||
match msg {
|
match msg {
|
||||||
MindMessage::Hotkey(HotkeyAction::CycleAutonomy) => {
|
MindMessage::Hotkey(HotkeyAction::CycleAutonomy) => {
|
||||||
let label = self.shared.lock().unwrap().cycle_autonomy();
|
self.shared.lock().unwrap().cycle_autonomy();
|
||||||
let _ = self.ui_tx.send(UiMessage::Info(
|
|
||||||
format!("DMN → {} (Ctrl+P to cycle)", label),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
MindMessage::Hotkey(HotkeyAction::Interrupt) => {
|
MindMessage::Hotkey(HotkeyAction::Interrupt) => {
|
||||||
self.shared.lock().unwrap().interrupt();
|
self.shared.lock().unwrap().interrupt();
|
||||||
|
|
@ -333,7 +331,6 @@ impl Mind {
|
||||||
if let Some(h) = self.turn_handle.take() { h.abort(); }
|
if let Some(h) = self.turn_handle.take() { h.abort(); }
|
||||||
self.shared.lock().unwrap().turn_active = false;
|
self.shared.lock().unwrap().turn_active = false;
|
||||||
let _ = self.turn_watch.send(false);
|
let _ = self.turn_watch.send(false);
|
||||||
let _ = self.ui_tx.send(UiMessage::Info("(interrupted)".into()));
|
|
||||||
}
|
}
|
||||||
MindMessage::NewSession => {
|
MindMessage::NewSession => {
|
||||||
self.shared.lock().unwrap().dmn_sleep();
|
self.shared.lock().unwrap().dmn_sleep();
|
||||||
|
|
@ -349,7 +346,6 @@ impl Mind {
|
||||||
self.config.app.clone(), self.config.prompt_file.clone(),
|
self.config.app.clone(), self.config.prompt_file.clone(),
|
||||||
new_log, shared_ctx, shared_tools,
|
new_log, shared_ctx, shared_tools,
|
||||||
);
|
);
|
||||||
let _ = self.ui_tx.send(UiMessage::Info("New session started.".into()));
|
|
||||||
}
|
}
|
||||||
MindMessage::Score => {
|
MindMessage::Score => {
|
||||||
let mut s = self.shared.lock().unwrap();
|
let mut s = self.shared.lock().unwrap();
|
||||||
|
|
@ -357,16 +353,12 @@ impl Mind {
|
||||||
s.scoring_in_flight = true;
|
s.scoring_in_flight = true;
|
||||||
drop(s);
|
drop(s);
|
||||||
self.start_memory_scoring();
|
self.start_memory_scoring();
|
||||||
} else {
|
|
||||||
let _ = self.ui_tx.send(UiMessage::Info("(scoring already in progress)".into()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
// Check for pending input
|
|
||||||
let action = self.shared.lock().unwrap().take_pending_input();
|
let action = self.shared.lock().unwrap().take_pending_input();
|
||||||
self.execute(action);
|
self.execute(action);
|
||||||
crate::user::event_loop::update_status(&self.shared, &self.ui_tx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((result, target)) = turn_rx.recv() => {
|
Some((result, target)) = turn_rx.recv() => {
|
||||||
|
|
@ -374,20 +366,12 @@ impl Mind {
|
||||||
let model_switch = self.shared.lock().unwrap().complete_turn(&result, target);
|
let model_switch = self.shared.lock().unwrap().complete_turn(&result, target);
|
||||||
let _ = self.turn_watch.send(false);
|
let _ = self.turn_watch.send(false);
|
||||||
|
|
||||||
if let Err(ref e) = result {
|
|
||||||
let msg = match target {
|
|
||||||
StreamTarget::Autonomous => UiMessage::DmnAnnotation(format!("[error: {:#}]", e)),
|
|
||||||
StreamTarget::Conversation => UiMessage::Info(format!("Error: {:#}", e)),
|
|
||||||
};
|
|
||||||
let _ = self.ui_tx.send(msg);
|
|
||||||
}
|
|
||||||
if let Some(name) = model_switch {
|
if let Some(name) = model_switch {
|
||||||
crate::user::event_loop::cmd_switch_model(&self.agent, &name, &self.ui_tx).await;
|
crate::user::event_loop::cmd_switch_model(&self.agent, &name, &self.ui_tx).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.check_compaction();
|
self.check_compaction();
|
||||||
if !self.config.no_agents { self.start_memory_scoring(); }
|
if !self.config.no_agents { self.start_memory_scoring(); }
|
||||||
crate::user::event_loop::update_status(&self.shared, &self.ui_tx);
|
|
||||||
|
|
||||||
let action = self.shared.lock().unwrap().take_pending_input();
|
let action = self.shared.lock().unwrap().take_pending_input();
|
||||||
self.execute(action);
|
self.execute(action);
|
||||||
|
|
@ -400,7 +384,6 @@ impl Mind {
|
||||||
let action = self.shared.lock().unwrap().dmn_tick();
|
let action = self.shared.lock().unwrap().dmn_tick();
|
||||||
self.execute(action);
|
self.execute(action);
|
||||||
}
|
}
|
||||||
crate::user::event_loop::update_status(&self.shared, &self.ui_tx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -500,7 +483,6 @@ pub async fn run(cli: crate::user::CliArgs) -> Result<()> {
|
||||||
let shared_mind = shared_mind_state(config.app.dmn.max_turns);
|
let shared_mind = shared_mind_state(config.app.dmn.max_turns);
|
||||||
crate::user::event_loop::send_context_info(&config, &ui_tx);
|
crate::user::event_loop::send_context_info(&config, &ui_tx);
|
||||||
let mut mind = Mind::new(agent, shared_mind.clone(), config, ui_tx.clone(), turn_tx);
|
let mut mind = Mind::new(agent, shared_mind.clone(), config, ui_tx.clone(), turn_tx);
|
||||||
crate::user::event_loop::update_status(&shared_mind, &ui_tx);
|
|
||||||
if !no_agents {
|
if !no_agents {
|
||||||
mind.start_memory_scoring();
|
mind.start_memory_scoring();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,18 +130,6 @@ fn cmd_adjust_sampling(agent: &Arc<Mutex<Agent>>, param: usize, delta: f32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_status(shared: &crate::mind::SharedMindState, ui_tx: &ui_channel::UiSender) {
|
|
||||||
let s = shared.lock().unwrap();
|
|
||||||
let _ = ui_tx.send(UiMessage::StatusUpdate(ui_channel::StatusInfo {
|
|
||||||
dmn_state: s.dmn.label().to_string(),
|
|
||||||
dmn_turns: s.dmn_turns,
|
|
||||||
dmn_max_turns: s.max_dmn_turns,
|
|
||||||
prompt_tokens: 0, completion_tokens: 0,
|
|
||||||
model: String::new(), turn_tools: 0,
|
|
||||||
context_budget: String::new(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_context_info(config: &crate::config::SessionConfig, ui_tx: &ui_channel::UiSender) {
|
pub fn send_context_info(config: &crate::config::SessionConfig, ui_tx: &ui_channel::UiSender) {
|
||||||
let context_groups = crate::config::get().context_groups.clone();
|
let context_groups = crate::config::get().context_groups.clone();
|
||||||
let (instruction_files, memory_files) = crate::mind::identity::context_file_info(
|
let (instruction_files, memory_files) = crate::mind::identity::context_file_info(
|
||||||
|
|
@ -216,6 +204,7 @@ pub async fn run(
|
||||||
let mut render_interval = tokio::time::interval(Duration::from_millis(50));
|
let mut render_interval = tokio::time::interval(Duration::from_millis(50));
|
||||||
render_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
render_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||||
let mut dirty = true;
|
let mut dirty = true;
|
||||||
|
let mut prev_mind = shared_mind.lock().unwrap().clone();
|
||||||
|
|
||||||
terminal.hide_cursor()?;
|
terminal.hide_cursor()?;
|
||||||
|
|
||||||
|
|
@ -266,6 +255,26 @@ pub async fn run(
|
||||||
idle_state.decay_ewma();
|
idle_state.decay_ewma();
|
||||||
app.update_idle(&idle_state);
|
app.update_idle(&idle_state);
|
||||||
|
|
||||||
|
// Diff MindState — generate UI messages from changes
|
||||||
|
{
|
||||||
|
let cur = shared_mind.lock().unwrap().clone();
|
||||||
|
if cur.dmn.label() != prev_mind.dmn.label() || cur.dmn_turns != prev_mind.dmn_turns {
|
||||||
|
let _ = ui_tx.send(UiMessage::StatusUpdate(ui_channel::StatusInfo {
|
||||||
|
dmn_state: cur.dmn.label().to_string(),
|
||||||
|
dmn_turns: cur.dmn_turns,
|
||||||
|
dmn_max_turns: cur.max_dmn_turns,
|
||||||
|
prompt_tokens: 0, completion_tokens: 0,
|
||||||
|
model: String::new(), turn_tools: 0,
|
||||||
|
context_budget: String::new(),
|
||||||
|
}));
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
if cur.turn_active != prev_mind.turn_active {
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
prev_mind = cur;
|
||||||
|
}
|
||||||
|
|
||||||
while let Ok(notif) = notify_rx.try_recv() {
|
while let Ok(notif) = notify_rx.try_recv() {
|
||||||
let tx = channel_tx.clone();
|
let tx = channel_tx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue