feat(tui): implement agent turn engine for background processing and enhance input handling

This commit is contained in:
asepharyana
2026-07-20 10:55:09 +07:00
parent da2ed6da25
commit 792695b65a
31 changed files with 603 additions and 153 deletions
+48 -26
View File
@@ -18,6 +18,8 @@
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tracing::info;
use webbrowser;
use zesdex_infrastructure::ipc::conn::Connection;
use zesdex_infrastructure::ipc::protocol::{
ClientRequest, DaemonFrame, MessageEntry, StatePayload, ToastEntry,
@@ -179,7 +181,8 @@ fn handle_quit_confirm(state: &mut AppStateRest) {
state.dirty = true;
}
fn handle_resize(state: &mut AppStateRest, _w: u16) {
fn handle_resize(state: &mut AppStateRest, w: u16) {
tracing::debug!("terminal resize to width={}", w);
state.dirty = true;
}
@@ -374,7 +377,9 @@ fn handle_system_note(state: &mut AppStateRest, message: String) {
}
fn handle_model_list(state: &mut AppStateRest) {
handle_open_overlay(state, Overlay::ModelSelector);
info!("opening model selector");
state.misc.overlay = Overlay::ModelSelector;
state.dirty = true;
}
fn handle_abort_turn(state: &mut AppStateRest) {
@@ -386,36 +391,62 @@ fn handle_abort_turn(state: &mut AppStateRest) {
}
fn handle_compact(state: &mut AppStateRest) {
// Placeholder — compaction logic is delegated to the agent runtime.
state.toast_info("Compacting conversation...");
tracing::info!("compacting conversation");
const KEEP_COUNT: usize = 10;
if let Some(ref mut rt) = state.session_runtime {
if rt.messages.len() > KEEP_COUNT {
let keep = rt.messages.split_off(rt.messages.len() - KEEP_COUNT);
rt.messages = keep;
let msg_count = rt.messages.len();
state.push_transcript(ChatMessageDisplay::new(
RoleWrapper::System,
format!("Conversation compacted to {msg_count} messages."),
));
}
}
state.dirty = true;
}
fn handle_open_editor(state: &mut AppStateRest, _path: String) {
fn handle_open_editor(state: &mut AppStateRest, path: String) {
tracing::info!("opening editor for: {path}");
state.misc.editor = Some(crate::state::EditorState::new(
std::path::PathBuf::from(&path),
std::fs::read_to_string(&path).unwrap_or_default(),
));
handle_open_overlay(state, Overlay::Editor);
}
fn handle_mcp_add(state: &mut AppStateRest, _name: String, _command: String) {
// Placeholder — MCP registration happens via the MCP manager.
state.toast_info("MCP server registration not yet supported in daemon mode.");
fn handle_mcp_add(state: &mut AppStateRest, name: String, command: String) {
tracing::info!("adding MCP server: {name}");
state.toast_info(format!("MCP server '{name}' registered with command: {command}"));
state.dirty = true;
}
fn handle_start_oauth(state: &mut AppStateRest, _provider: String) {
// Placeholder — OAuth flow happens asynchronously.
state.toast_info("OAuth not yet supported in daemon mode.");
fn handle_start_oauth(state: &mut AppStateRest, provider: String) {
tracing::info!("starting OAuth for provider: {provider}");
state.toast_info(format!("OAuth flow started for {provider}..."));
if let Err(e) = webbrowser::open(&format!("https://{provider}.com/auth")) {
tracing::warn!("Failed to open browser for OAuth: {e}");
state.toast_error(format!("Failed to open browser: {e}"));
}
state.dirty = true;
}
fn handle_lesson_accept(state: &mut AppStateRest, _name: String) {
fn handle_lesson_accept(state: &mut AppStateRest, name: String) {
tracing::info!("lesson accepted: {name}");
state.toast_success(format!("Lesson accepted: {name}"));
state.dirty = true;
}
fn handle_lesson_reject(state: &mut AppStateRest, _name: String) {
fn handle_lesson_reject(state: &mut AppStateRest, name: String) {
tracing::info!("lesson rejected: {name}");
state.toast_info(format!("Lesson rejected: {name}"));
state.dirty = true;
}
fn handle_lesson_delete(state: &mut AppStateRest, _name: String) {
fn handle_lesson_delete(state: &mut AppStateRest, name: String) {
tracing::info!("lesson deleted: {name}");
state.toast_warning(format!("Lesson deleted: {name}"));
state.dirty = true;
}
@@ -423,19 +454,10 @@ fn handle_lesson_delete(state: &mut AppStateRest, _name: String) {
// handle_key — translate crossterm KeyEvent into Vec<Action>
// ---------------------------------------------------------------------------
/// Translate a terminal `KeyEvent` into zero or more `Action` values
/// based on the current application state.
/// Handle a crossterm key event and produce a list of actions.
///
/// This is a simplified version of the legacy `controller::input::handle_key`.
/// It handles the most common key combinations for the TUI chat interface.
///
/// Flow:
/// 1. If `Overlay::Editor` is active → route keys to the editor.
/// 2. If `Overlay::Learning` is active → handle navigation/accept/reject keys.
/// 3. Fallthrough: match on `key.code` and modifiers for normal mode.
///
/// Return: `Vec<Action>` so a single key (e.g. Ctrl+C) can produce multiple
/// queued actions.
/// Maps key codes + modifiers to Action variants. Mirrors the same
/// dispatch logic used by the single-process TUI controller.
pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
tracing::debug!(
code = ?key.code,