feat: centralize default constants and refactor overlay enter handling in TUI
This commit is contained in:
@@ -29,7 +29,8 @@ use crate::state::create_session;
|
||||
/// save settings, and release the lock.
|
||||
pub fn run_daemon() -> Result<()> {
|
||||
tracing::info!("starting daemon process");
|
||||
let (store, _session_lock_guard, mut state, _rt) = create_session()?;
|
||||
let (store, _session_lock_guard, mut state, rt) = create_session()?;
|
||||
let _guard = rt.enter();
|
||||
|
||||
let run_dir = store.base_dir.join("run");
|
||||
std::fs::create_dir_all(&run_dir)?;
|
||||
|
||||
@@ -15,6 +15,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::controller::command::{apply_command, parse_command};
|
||||
use crate::controller::overlay_enter::handle_overlay_enter;
|
||||
use crate::state::{AutocompleteKind, Overlay, AppStateRest};
|
||||
|
||||
/// Mark state dirty and return an empty action list.
|
||||
@@ -329,112 +330,6 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle pressing Enter while a modal overlay is active.
|
||||
///
|
||||
/// Each overlay variant has its own Enter semantics:
|
||||
/// - `QuitConfirm` → set `quit = true`
|
||||
/// - `KeyInput` → save API key from buffer
|
||||
/// - `ModelSelector` → switch provider/model from selected index
|
||||
/// - `ClearConfirm` → clear transcript cache
|
||||
/// - `Rewind` → rewind to selected message index
|
||||
/// - `Bash` / `Settings` / `Todo` / `Mcp` → no-ops (placeholder)
|
||||
#[tracing::instrument(skip(state))]
|
||||
fn handle_overlay_enter(state: &mut AppStateRest) -> Vec<Action> {
|
||||
debug!(overlay = ?state.misc.overlay, "handle_overlay_enter");
|
||||
match state.misc.overlay {
|
||||
Overlay::Bash => {
|
||||
let command = state.input.buffer.clone();
|
||||
state.toast_info(format!("Submitting bash command: {command}"));
|
||||
state.input.buffer.clear();
|
||||
state.input.cursor = 0;
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::Settings => {
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::Todo => {
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::QuitConfirm => {
|
||||
state.quit = true;
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::KeyInput => {
|
||||
let text = state.input.buffer.clone();
|
||||
if !text.is_empty() {
|
||||
state
|
||||
.settings
|
||||
.api_keys
|
||||
.insert(state.settings.provider.clone(), text);
|
||||
}
|
||||
state.toast_success("API key saved".to_string());
|
||||
state.input.buffer.clear();
|
||||
state.input.cursor = 0;
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.save_settings();
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::Mcp => {
|
||||
state.toast_info("Connecting MCP...".to_string());
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::Rewind => {
|
||||
let idx = state.misc.selected_index;
|
||||
let n = state.transcript_cache.messages.len();
|
||||
if idx < n {
|
||||
let rewind_to = n - idx - 1;
|
||||
state.push_transcript(crate::state::ChatMessageDisplay::new(
|
||||
zesdex_domain::core::Role::System,
|
||||
format!("Rewound to message {rewind_to}"),
|
||||
));
|
||||
}
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::ModelSelector => {
|
||||
let providers: Vec<String> = state.app_config.providers.keys().cloned().collect();
|
||||
if let Some(provider) = providers.get(state.misc.selected_index) {
|
||||
if let Some(cfg) = state.app_config.providers.get(provider) {
|
||||
let model = cfg.default_model.clone().unwrap_or_else(|| {
|
||||
"claude-opus-4-8".to_string()
|
||||
});
|
||||
state.settings.provider.clone_from(provider);
|
||||
state.settings.model.clone_from(&model);
|
||||
if let Some(ref key) = cfg.default_api_key {
|
||||
state.settings.api_keys.insert(provider.clone(), key.clone());
|
||||
} else if let Some(env_key) = cfg
|
||||
.api_key_env
|
||||
.as_ref()
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
{
|
||||
state.settings.api_keys.insert(provider.clone(), env_key);
|
||||
}
|
||||
state.save_settings();
|
||||
state.toast_success(format!("Switched to {provider} / {model}"));
|
||||
}
|
||||
}
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::ClearConfirm => {
|
||||
state.toast_info("Transcript cleared".to_string());
|
||||
state.transcript_cache.messages.clear();
|
||||
state.transcript_cache.dirty = true;
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
//! commands into structured `Action` variants.
|
||||
pub mod command;
|
||||
pub mod input;
|
||||
pub mod overlay_enter;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
//! Overlay-specific Enter-key handlers.
|
||||
//!
|
||||
//! Each overlay variant has its own Enter semantics. Extracted from the
|
||||
//! monolithic `input.rs` so each handler is self-contained.
|
||||
|
||||
use tracing::debug;
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::state::{AppStateRest, Overlay};
|
||||
|
||||
/// Handle pressing Enter while a modal overlay is active.
|
||||
///
|
||||
/// Each overlay variant has its own Enter semantics:
|
||||
/// - `QuitConfirm` → set `quit = true`
|
||||
/// - `KeyInput` → save API key from buffer
|
||||
/// - `ModelSelector` → switch provider/model from selected index
|
||||
/// - `ClearConfirm` → clear transcript cache
|
||||
/// - `Rewind` → rewind to selected message index
|
||||
/// - `Bash` / `Settings` / `Todo` / `Mcp` → no-ops (placeholder)
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub fn handle_overlay_enter(state: &mut AppStateRest) -> Vec<Action> {
|
||||
debug!(overlay = ?state.misc.overlay, "handle_overlay_enter");
|
||||
match state.misc.overlay {
|
||||
Overlay::Bash => handle_bash_enter(state),
|
||||
Overlay::Settings | Overlay::Todo => {
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::QuitConfirm => {
|
||||
state.quit = true;
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::KeyInput => handle_keyinput_enter(state),
|
||||
Overlay::Mcp => {
|
||||
state.toast_info("Connecting MCP...".to_string());
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::Rewind => handle_rewind_enter(state),
|
||||
Overlay::ModelSelector => handle_model_selector_enter(state),
|
||||
Overlay::ClearConfirm => handle_clear_confirm(state),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_bash_enter(state: &mut AppStateRest) -> Vec<Action> {
|
||||
let command = state.input.buffer.clone();
|
||||
state.toast_info(format!("Submitting bash command: {command}"));
|
||||
state.input.buffer.clear();
|
||||
state.input.cursor = 0;
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn handle_keyinput_enter(state: &mut AppStateRest) -> Vec<Action> {
|
||||
let text = state.input.buffer.clone();
|
||||
if !text.is_empty() {
|
||||
state
|
||||
.settings
|
||||
.api_keys
|
||||
.insert(state.settings.provider.clone(), text);
|
||||
}
|
||||
state.toast_success("API key saved".to_string());
|
||||
state.input.buffer.clear();
|
||||
state.input.cursor = 0;
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.save_settings();
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn handle_rewind_enter(state: &mut AppStateRest) -> Vec<Action> {
|
||||
let idx = state.misc.selected_index;
|
||||
let n = state.transcript_cache.messages.len();
|
||||
if idx < n {
|
||||
let rewind_to = n - idx - 1;
|
||||
state.push_transcript(crate::state::ChatMessageDisplay::new(
|
||||
zesdex_domain::core::Role::System,
|
||||
format!("Rewound to message {rewind_to}"),
|
||||
));
|
||||
}
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn handle_model_selector_enter(state: &mut AppStateRest) -> Vec<Action> {
|
||||
let providers: Vec<String> = state.app_config.providers.keys().cloned().collect();
|
||||
if let Some(provider) = providers.get(state.misc.selected_index) {
|
||||
if let Some(cfg) = state.app_config.providers.get(provider) {
|
||||
let model = cfg
|
||||
.default_model
|
||||
.clone()
|
||||
.unwrap_or_else(|| zesdex_domain::agent::defaults::DEFAULT_MODEL.to_string());
|
||||
state.settings.provider.clone_from(provider);
|
||||
state.settings.model.clone_from(&model);
|
||||
if let Some(ref key) = cfg.default_api_key {
|
||||
state.settings.api_keys.insert(provider.clone(), key.clone());
|
||||
} else if let Some(env_key) = cfg
|
||||
.api_key_env
|
||||
.as_ref()
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
{
|
||||
state.settings.api_keys.insert(provider.clone(), env_key);
|
||||
}
|
||||
state.save_settings();
|
||||
state.toast_success(format!("Switched to {provider} / {model}"));
|
||||
}
|
||||
}
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn handle_clear_confirm(state: &mut AppStateRest) -> Vec<Action> {
|
||||
state.toast_info("Transcript cleared".to_string());
|
||||
state.transcript_cache.messages.clear();
|
||||
state.transcript_cache.dirty = true;
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.mark_dirty();
|
||||
Vec::new()
|
||||
}
|
||||
@@ -30,6 +30,9 @@ use crate::view;
|
||||
/// save settings.
|
||||
#[tracing::instrument]
|
||||
pub fn run_single_process() -> Result<()> {
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
let _guard = rt.enter();
|
||||
|
||||
// Create session state
|
||||
info!("starting single-process TUI");
|
||||
let (_store, mut state) = create_local_session()?;
|
||||
|
||||
@@ -130,3 +130,28 @@ pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) {
|
||||
let _ = turn_service.run_turn(params).await;
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_spawn_agent_turn_with_tokio_runtime() {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let _guard = rt.enter();
|
||||
|
||||
let temp_dir = std::env::temp_dir().join(format!("zesdex_test_{}", uuid::Uuid::new_v4()));
|
||||
let session_dir = temp_dir.join("session");
|
||||
let memory_dir = temp_dir.join("memory");
|
||||
std::fs::create_dir_all(&session_dir).unwrap();
|
||||
std::fs::create_dir_all(&memory_dir).unwrap();
|
||||
|
||||
let workspace_roots = vec![temp_dir.clone()];
|
||||
let mut state = AppStateRest::new(workspace_roots, &session_dir, memory_dir);
|
||||
|
||||
spawn_agent_turn(&mut state, "hello".to_string());
|
||||
assert!(state.turn_in_flight());
|
||||
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user