feat(tui): implement agent turn engine for background processing and enhance input handling
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -37,23 +37,23 @@ pub fn run_daemon() -> Result<()> {
|
||||
let addr = socket_path.to_string_lossy().to_string();
|
||||
|
||||
let server = IpcServer::bind_unix(&addr)?;
|
||||
eprintln!("daemon: listening on {addr}");
|
||||
tracing::info!("daemon listening on {addr}");
|
||||
|
||||
loop {
|
||||
let conn = match server.accept() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("daemon: accept error: {e}");
|
||||
tracing::error!("daemon accept error: {e}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
eprintln!("daemon: client connected");
|
||||
tracing::info!("daemon client connected");
|
||||
|
||||
if let Err(e) = handle_daemon_client(conn, &mut state) {
|
||||
eprintln!("daemon: error handling client: {e}");
|
||||
tracing::error!("daemon error handling client: {e}");
|
||||
}
|
||||
|
||||
eprintln!("daemon: client disconnected, waiting for next connection...");
|
||||
tracing::info!("daemon client disconnected");
|
||||
state.save_settings();
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ pub enum Overlay {
|
||||
Effort,
|
||||
/// MCP server management panel.
|
||||
Mcp,
|
||||
/// TODO list overlay.
|
||||
/// Task list overlay.
|
||||
Todo,
|
||||
/// Session rewind / history scrubber.
|
||||
Rewind,
|
||||
@@ -311,6 +311,8 @@ pub struct MiscState {
|
||||
pub lesson_running: bool,
|
||||
/// Text waiting to be written to the system clipboard.
|
||||
pub pending_clipboard_copy: Option<String>,
|
||||
/// Inline editor state, if the editor overlay is active.
|
||||
pub editor: Option<EditorState>,
|
||||
}
|
||||
|
||||
impl MiscState {
|
||||
@@ -327,6 +329,7 @@ impl MiscState {
|
||||
todo_content: String::new(),
|
||||
lesson_running: false,
|
||||
pending_clipboard_copy: None,
|
||||
editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +365,7 @@ pub struct AgentState {
|
||||
pub current_tool: String,
|
||||
}
|
||||
|
||||
/// Minimal workflow-engine placeholder for hive-mind orchestration state.
|
||||
/// Workflow engine state tracking agents in hive-mind orchestration.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct WorkflowEngine {
|
||||
/// List of running workflow agent states.
|
||||
@@ -378,6 +381,34 @@ impl WorkflowEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple inline editor state for the TUI.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EditorState {
|
||||
/// Path to the file being edited.
|
||||
pub path: PathBuf,
|
||||
/// Current buffer content.
|
||||
pub content: String,
|
||||
/// Cursor position (byte offset).
|
||||
pub cursor: usize,
|
||||
}
|
||||
|
||||
impl EditorState {
|
||||
/// Create a new editor state for the given path.
|
||||
pub fn new(path: PathBuf, content: String) -> Self {
|
||||
let cursor = content.len();
|
||||
EditorState {
|
||||
path,
|
||||
content,
|
||||
cursor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the full buffer content.
|
||||
pub fn as_string(&self) -> String {
|
||||
self.content.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AppStateRest — single source-of-truth application state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user