From d615090dcd34bdb93be22c1c7ce4ba674363c258 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Tue, 21 Jul 2026 07:00:15 +0700 Subject: [PATCH] feat: centralize default constants and refactor overlay enter handling in TUI --- apps/domain/src/agent/defaults.rs | 34 +++++ apps/domain/src/agent/mod.rs | 1 + apps/domain/src/lib.rs | 1 + apps/domain/src/subagent/mod.rs | 26 ---- apps/gateway/src/main.rs | 6 +- apps/infrastructure/src/lib.rs | 1 - apps/infrastructure/src/llm/provider.rs | 4 +- apps/infrastructure/src/review/mod.rs | 8 -- apps/infrastructure/src/review/pending.rs | 43 ------ apps/infrastructure/src/review/probe.rs | 18 --- apps/infrastructure/src/review/prompt.rs | 25 ---- apps/infrastructure/src/review/staleness.rs | 15 --- apps/infrastructure/src/review/types.rs | 29 ----- .../src/subagent/auto/engine.rs | 2 +- apps/infrastructure/src/subagent/event.rs | 3 - apps/infrastructure/src/subagent/gating.rs | 23 ---- apps/infrastructure/src/subagent/mod.rs | 4 - apps/infrastructure/src/subagent/provider.rs | 4 +- apps/infrastructure/src/subagent/tools.rs | 19 --- apps/infrastructure/src/subagent/workspace.rs | 16 --- .../src/tools/parallel_delegate.rs | 2 +- apps/infrastructure/src/tools/plan.rs | 10 +- apps/infrastructure/src/tools/spawn.rs | 4 +- .../src/tools/utility/todofinish.rs | 5 +- .../src/tools/utility/todowrite.rs | 5 +- apps/infrastructure/src/tools/workflow.rs | 2 +- .../src/workflow/engine/primitives.rs | 2 +- .../src/workflow/hive_mind/cycle.rs | 2 +- apps/interfaces/daemon/src/server.rs | 3 +- apps/interfaces/tui/src/controller/input.rs | 107 +-------------- apps/interfaces/tui/src/controller/mod.rs | 1 + .../tui/src/controller/overlay_enter.rs | 122 ++++++++++++++++++ apps/interfaces/tui/src/run.rs | 3 + apps/interfaces/tui/src/turn.rs | 25 ++++ 34 files changed, 216 insertions(+), 359 deletions(-) create mode 100644 apps/domain/src/agent/defaults.rs delete mode 100644 apps/infrastructure/src/review/mod.rs delete mode 100644 apps/infrastructure/src/review/pending.rs delete mode 100644 apps/infrastructure/src/review/probe.rs delete mode 100644 apps/infrastructure/src/review/prompt.rs delete mode 100644 apps/infrastructure/src/review/staleness.rs delete mode 100644 apps/infrastructure/src/review/types.rs delete mode 100644 apps/infrastructure/src/subagent/event.rs delete mode 100644 apps/infrastructure/src/subagent/gating.rs delete mode 100644 apps/infrastructure/src/subagent/tools.rs delete mode 100644 apps/infrastructure/src/subagent/workspace.rs create mode 100644 apps/interfaces/tui/src/controller/overlay_enter.rs diff --git a/apps/domain/src/agent/defaults.rs b/apps/domain/src/agent/defaults.rs new file mode 100644 index 0000000..b7475df --- /dev/null +++ b/apps/domain/src/agent/defaults.rs @@ -0,0 +1,34 @@ +//! Shared default constants used across the application. +//! +//! Centralising these values eliminates the hardcoded-string duplication +//! that existed when every call site provided its own inline fallback. +//! Consumers should reference these constants rather than repeating +//! the string literals. + +/// Default LLM provider API base URL. +pub const DEFAULT_API_BASE: &str = "https://opencode.ai/zen/v1"; + +/// Default LLM model identifier. +pub const DEFAULT_MODEL: &str = "deepseek-v4-flash-free"; + +/// Fallback JWT secret used only when `JWT_SECRET` env var is unset. +/// In production this MUST be configured via environment variable. +pub const FALLBACK_JWT_SECRET: &str = "dev-secret"; + +/// Default context window size (128k tokens). +pub const DEFAULT_CONTEXT_WINDOW: usize = 256_000; + +/// Maximum tool-call iterations per agent turn. +pub const MAX_TOOL_ITERATIONS: u32 = 50; + +/// Maximum subagent tool-call iterations. +pub const MAX_SUBAGENT_ITERATIONS: u32 = 25; + +/// Default LLM request max tokens. +pub const DEFAULT_MAX_TOKENS: u32 = 4096; + +/// Default temperature for the main agent. +pub const DEFAULT_TEMPERATURE: f64 = 0.7; + +/// Default temperature for compaction / summary calls. +pub const DEFAULT_COMPACT_TEMPERATURE: f64 = 0.3; diff --git a/apps/domain/src/agent/mod.rs b/apps/domain/src/agent/mod.rs index 23a9037..dae7e30 100644 --- a/apps/domain/src/agent/mod.rs +++ b/apps/domain/src/agent/mod.rs @@ -6,6 +6,7 @@ use std::path::PathBuf; use crate::core::{ChatMessage, ToolCallResult, UsageStats}; +pub mod defaults; pub mod prompt; pub mod progress; diff --git a/apps/domain/src/lib.rs b/apps/domain/src/lib.rs index 4fa7b24..a26bd25 100644 --- a/apps/domain/src/lib.rs +++ b/apps/domain/src/lib.rs @@ -57,6 +57,7 @@ pub use error::DomainError; // Agent module top-level items (TurnEvent, SessionRuntime, etc.) pub use agent::*; // Sub-module items need explicit re-exports +pub use agent::defaults::*; pub use agent::progress::AgentProgress; pub use agent::prompt::{compaction_prompt, main_agent_prompt, subagent_directive}; pub use workflow::*; diff --git a/apps/domain/src/subagent/mod.rs b/apps/domain/src/subagent/mod.rs index 9416471..19021b2 100644 --- a/apps/domain/src/subagent/mod.rs +++ b/apps/domain/src/subagent/mod.rs @@ -1,31 +1,5 @@ //! Subagent domain models. -/// Events emitted by a running subagent. -#[derive(Debug, Clone)] -pub enum SubagentEvent { - Started { - agent_id: String, - directive: String, - }, - ToolCall { - agent_id: String, - tool_name: String, - }, - ToolResult { - agent_id: String, - tool_name: String, - output: String, - }, - Completed { - agent_id: String, - output: String, - }, - Failed { - agent_id: String, - error: String, - }, -} - /// Access tier for subagent tool permissions. /// /// Tiers are cumulative: `Write` includes everything in `Read`, and `Full` diff --git a/apps/gateway/src/main.rs b/apps/gateway/src/main.rs index b90635e..c2648d0 100644 --- a/apps/gateway/src/main.rs +++ b/apps/gateway/src/main.rs @@ -144,15 +144,15 @@ fn run_api_server(port: u16) -> anyhow::Result<()> { "JWT_SECRET environment variable not set; using insecure default. \ Set JWT_SECRET to a secure random value in production." ); - "dev-secret".to_string() + zesdex_domain::agent::defaults::FALLBACK_JWT_SECRET.to_string() }); let state = zesdex_api::ApiState::new( store.base_dir.clone(), jwt_secret, "", - "deepseek-v4-flash-free", - Some("https://opencode.ai/zen/v1".to_string()), + zesdex_domain::agent::defaults::DEFAULT_MODEL, + Some(zesdex_domain::agent::defaults::DEFAULT_API_BASE.to_string()), ); let app = zesdex_api::build_router(state); let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port)); diff --git a/apps/infrastructure/src/lib.rs b/apps/infrastructure/src/lib.rs index 93d453f..8652eab 100644 --- a/apps/infrastructure/src/lib.rs +++ b/apps/infrastructure/src/lib.rs @@ -35,7 +35,6 @@ pub mod lsp; pub mod mcp; pub mod middleware; pub mod persistence; -pub mod review; pub mod subagent; pub mod tools; pub mod utils; diff --git a/apps/infrastructure/src/llm/provider.rs b/apps/infrastructure/src/llm/provider.rs index 0d21948..c781821 100644 --- a/apps/infrastructure/src/llm/provider.rs +++ b/apps/infrastructure/src/llm/provider.rs @@ -11,8 +11,8 @@ use zesdex_domain::core::{ }; use zesdex_application::ports::ProviderService; -const DEFAULT_BASE_URL: &str = "https://opencode.ai/zen/v1"; -const DEFAULT_MODEL: &str = "deepseek-v4-flash-free"; +use zesdex_domain::agent::defaults::{DEFAULT_API_BASE, DEFAULT_MODEL}; +const DEFAULT_BASE_URL: &str = DEFAULT_API_BASE; pub const DEFAULT_API_KEY: &str = ""; const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); const REQUEST_TIMEOUT: Duration = Duration::from_secs(600); diff --git a/apps/infrastructure/src/review/mod.rs b/apps/infrastructure/src/review/mod.rs deleted file mode 100644 index fea4f2f..0000000 --- a/apps/infrastructure/src/review/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Post-edit auto-review subagent — validates file edits and suggests -//! improvements. - -pub mod pending; -pub mod probe; -pub mod prompt; -pub mod staleness; -pub mod types; diff --git a/apps/infrastructure/src/review/pending.rs b/apps/infrastructure/src/review/pending.rs deleted file mode 100644 index 1990a87..0000000 --- a/apps/infrastructure/src/review/pending.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Pending review queue — tracks files modified by tools that have not -//! yet been reviewed. - -use std::collections::VecDeque; - -/// A file mutation awaiting review. -#[derive(Debug, Clone)] -pub struct PendingReview { - pub path: String, - pub tool: String, - pub reason: String, - pub content_sha256: String, -} - -/// Queue of files modified but not yet reviewed. -#[derive(Debug, Clone, Default)] -pub struct PendingReviewQueue { - entries: VecDeque, -} - -impl PendingReviewQueue { - pub fn new() -> Self { - PendingReviewQueue { - entries: VecDeque::new(), - } - } - - pub fn push(&mut self, entry: PendingReview) { - self.entries.push_back(entry); - } - - pub fn pop(&mut self) -> Option { - self.entries.pop_front() - } - - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } - - pub fn len(&self) -> usize { - self.entries.len() - } -} diff --git a/apps/infrastructure/src/review/probe.rs b/apps/infrastructure/src/review/probe.rs deleted file mode 100644 index 86b17b8..0000000 --- a/apps/infrastructure/src/review/probe.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Review probe — diff analysis and file inspection for review purposes. - -use similar::{ChangeTag, TextDiff}; - -/// Compute a simple unified diff between old and new text. -pub fn compute_diff(old: &str, new: &str) -> String { - let diff = TextDiff::from_lines(old, new); - let mut result = String::new(); - for change in diff.iter_all_changes() { - let sign = match change.tag() { - ChangeTag::Delete => "-", - ChangeTag::Insert => "+", - ChangeTag::Equal => " ", - }; - result.push_str(&format!("{}{}", sign, change.value())); - } - result -} diff --git a/apps/infrastructure/src/review/prompt.rs b/apps/infrastructure/src/review/prompt.rs deleted file mode 100644 index 14d60be..0000000 --- a/apps/infrastructure/src/review/prompt.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Review prompt construction — builds the system prompt for the -//! auto-review subagent. - -/// Build the review system prompt for the given diff and context. -pub fn build_review_prompt(diff: &str, file_path: &str) -> String { - format!( - "You are a code reviewer. Review the following diff for file '{}':\n\ - \n\ - Focus on:\n\ - 1. Correctness — does the change introduce bugs?\n\ - 2. Security — does the change introduce vulnerabilities?\n\ - 3. Style — does the change follow best practices?\n\ - 4. Edge cases — are there unhandled edge cases?\n\ - \n\ - Diff:\n\ - ```diff\n\ - {}\n\ - ```\n\ - \n\ - Provide your review as a JSON array of findings with \ - 'severity' (Info/Warning/Error), 'file', 'line' (optional), \ - 'message', and 'suggestion' (optional).", - file_path, diff - ) -} diff --git a/apps/infrastructure/src/review/staleness.rs b/apps/infrastructure/src/review/staleness.rs deleted file mode 100644 index 74f3116..0000000 --- a/apps/infrastructure/src/review/staleness.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Staleness detection for lesson cache entries. - -use std::time::{SystemTime, UNIX_EPOCH}; - -/// How long (in seconds) before a lesson is considered stale. -const STALE_THRESHOLD_SECS: u64 = 86400 * 7; // 7 days - -/// Check whether a lesson timestamp is stale. -pub fn is_stale(updated_at: i64) -> bool { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64; - now.saturating_sub(updated_at) > STALE_THRESHOLD_SECS as i64 -} diff --git a/apps/infrastructure/src/review/types.rs b/apps/infrastructure/src/review/types.rs deleted file mode 100644 index acfb6d4..0000000 --- a/apps/infrastructure/src/review/types.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Review types — findings, severity, and configuration. - -use serde::{Deserialize, Serialize}; - -/// Severity of a review finding. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum ReviewSeverity { - Info, - Warning, - Error, -} - -/// A single review finding from the auto-review subagent. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReviewFinding { - pub severity: ReviewSeverity, - pub file: String, - pub line: Option, - pub message: String, - pub suggestion: Option, -} - -/// Configuration for the auto-review subagent. -#[derive(Debug, Clone)] -pub struct ReviewConfig { - pub max_lessons_per_run: usize, - pub adaptive_max_skip: u32, - pub enabled: bool, -} diff --git a/apps/infrastructure/src/subagent/auto/engine.rs b/apps/infrastructure/src/subagent/auto/engine.rs index 53e7b82..d069957 100644 --- a/apps/infrastructure/src/subagent/auto/engine.rs +++ b/apps/infrastructure/src/subagent/auto/engine.rs @@ -109,7 +109,7 @@ pub fn spawn_background_review( // 3. Resolve LLM credentials let base_url = api_base.unwrap_or_else(|| { std::env::var("OPENAI_API_BASE") - .unwrap_or_else(|_| "https://opencode.ai/zen/v1".to_string()) + .unwrap_or_else(|_| zesdex_domain::agent::defaults::DEFAULT_API_BASE.to_string()) }); let client = LlmClient::new(api_key, model, Some(base_url)); diff --git a/apps/infrastructure/src/subagent/event.rs b/apps/infrastructure/src/subagent/event.rs deleted file mode 100644 index 5545e53..0000000 --- a/apps/infrastructure/src/subagent/event.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Subagent event types — events emitted during subagent execution. - -pub use zesdex_domain::subagent::SubagentEvent; diff --git a/apps/infrastructure/src/subagent/gating.rs b/apps/infrastructure/src/subagent/gating.rs deleted file mode 100644 index 84d82fa..0000000 --- a/apps/infrastructure/src/subagent/gating.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Subagent gating — decide whether to run review/test/arch agents based -//! on the current context. - -use tracing::instrument; - -/// Determine whether an auto-review should be triggered after an edit. -/// -/// Gating logic: -/// - Returns `false` if there are no edits (`edit_count == 0`). -/// - Returns `false` if `consecutive_empty_reviews >= max_skip` (too many -/// consecutive reviews produced no findings, so skip further reviews). -/// - Otherwise returns `true`. -#[instrument] -pub fn should_review(edit_count: u32, consecutive_empty_reviews: u32, max_skip: u32) -> bool { - if edit_count == 0 { - return false; - } - // Skip review if we've had several consecutive empty reviews - if consecutive_empty_reviews >= max_skip { - return false; - } - true -} diff --git a/apps/infrastructure/src/subagent/mod.rs b/apps/infrastructure/src/subagent/mod.rs index 07bc11b..8f5b250 100644 --- a/apps/infrastructure/src/subagent/mod.rs +++ b/apps/infrastructure/src/subagent/mod.rs @@ -5,9 +5,5 @@ pub mod auto; pub mod context; pub mod division; pub mod engine; -pub mod event; -pub mod gating; pub mod provider; pub mod spawn; -pub mod tools; -pub mod workspace; diff --git a/apps/infrastructure/src/subagent/provider.rs b/apps/infrastructure/src/subagent/provider.rs index 72c2e14..786123f 100644 --- a/apps/infrastructure/src/subagent/provider.rs +++ b/apps/infrastructure/src/subagent/provider.rs @@ -67,7 +67,7 @@ impl SubagentProvider { /// /// Flow: reads `settings.provider` and `settings.model` → if model is empty, /// falls back to the provider config's `default_model` → if that is also -/// empty, uses `"deepseek-v4-flash-free"` as the ultimate default. +/// empty, uses the domain default model constant. #[instrument] pub fn resolve_subagent_provider( settings: &zesdex_domain::cms::Settings, @@ -82,7 +82,7 @@ pub fn resolve_subagent_provider( .providers .get(&provider) .and_then(|p| p.default_model.clone()) - .unwrap_or_else(|| "deepseek-v4-flash-free".to_string()) + .unwrap_or_else(|| zesdex_domain::agent::defaults::DEFAULT_MODEL.to_string()) } else { model }; diff --git a/apps/infrastructure/src/subagent/tools.rs b/apps/infrastructure/src/subagent/tools.rs deleted file mode 100644 index fe1b75b..0000000 --- a/apps/infrastructure/src/subagent/tools.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Subagent tool helpers — wrap tool execution for subagent use. - -use anyhow::Result; -use tracing::instrument; - -use crate::tools::{Tool, ToolCtx}; - -/// Execute a single tool call within a subagent context. -/// -/// Delegates directly to the tool's `run` method with the given context and -/// JSON arguments. -#[instrument(skip(tool, ctx, args))] -pub fn execute_tool_call( - tool: &dyn Tool, - ctx: &ToolCtx, - args: &serde_json::Value, -) -> Result { - tool.run(ctx, args) -} diff --git a/apps/infrastructure/src/subagent/workspace.rs b/apps/infrastructure/src/subagent/workspace.rs deleted file mode 100644 index 7dcf93e..0000000 --- a/apps/infrastructure/src/subagent/workspace.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Subagent workspace management — create isolated workspaces for subagents. - -use std::path::{Path, PathBuf}; - -use tracing::instrument; - -/// Create an isolated workspace directory for a subagent. -/// -/// Creates `{base_dir}/subagent-workspaces/{agent_id}` and all parent -/// directories if they do not already exist. -#[instrument] -pub fn create_subagent_workspace(base_dir: &Path, agent_id: &str) -> anyhow::Result { - let ws = base_dir.join("subagent-workspaces").join(agent_id); - std::fs::create_dir_all(&ws)?; - Ok(ws) -} diff --git a/apps/infrastructure/src/tools/parallel_delegate.rs b/apps/infrastructure/src/tools/parallel_delegate.rs index 4801fec..2772865 100644 --- a/apps/infrastructure/src/tools/parallel_delegate.rs +++ b/apps/infrastructure/src/tools/parallel_delegate.rs @@ -100,7 +100,7 @@ impl Tool for ParallelDelegate { .providers .get(&provider) .map(|p| p.api_base.clone()) - .unwrap_or_else(|| "https://opencode.ai/zen/v1".to_string()); + .unwrap_or_else(|| zesdex_domain::agent::defaults::DEFAULT_API_BASE.to_string()); let api_key = crate::llm::provider::resolve_api_key(&settings, &app_config); diff --git a/apps/infrastructure/src/tools/plan.rs b/apps/infrastructure/src/tools/plan.rs index 768ff98..0703b26 100644 --- a/apps/infrastructure/src/tools/plan.rs +++ b/apps/infrastructure/src/tools/plan.rs @@ -45,8 +45,9 @@ impl Tool for PlanEnter { let plan_path = ctx.session_dir.join("PLAN.md"); let _ = std::fs::write(&plan_path, &plan_text); if let Some(events) = &ctx.turn_events { - let mut q = events.lock().unwrap(); - q.push_back(crate::TurnEvent::PlanUpdate(plan_text.clone())); + if let Ok(mut q) = events.lock() { + q.push_back(crate::TurnEvent::PlanUpdate(plan_text.clone())); + } } Ok(format!( @@ -103,8 +104,9 @@ impl Tool for PlanReady { let plan_path = ctx.session_dir.join("PLAN.md"); let _ = std::fs::write(&plan_path, &plan_content); if let Some(events) = &ctx.turn_events { - let mut q = events.lock().unwrap(); - q.push_back(crate::TurnEvent::PlanUpdate(plan_content.clone())); + if let Ok(mut q) = events.lock() { + q.push_back(crate::TurnEvent::PlanUpdate(plan_content.clone())); + } } Ok(format!("Plan saved to {filename}. Starting execution.")) diff --git a/apps/infrastructure/src/tools/spawn.rs b/apps/infrastructure/src/tools/spawn.rs index 9dc869a..15692fb 100644 --- a/apps/infrastructure/src/tools/spawn.rs +++ b/apps/infrastructure/src/tools/spawn.rs @@ -79,7 +79,7 @@ impl Tool for SpawnAgents { .providers .get(&provider) .map(|p| p.api_base.clone()) - .unwrap_or_else(|| "https://opencode.ai/zen/v1".to_string()); + .unwrap_or_else(|| zesdex_domain::agent::defaults::DEFAULT_API_BASE.to_string()); let api_key = crate::llm::provider::resolve_api_key(&settings, &app_config); @@ -196,7 +196,7 @@ impl Tool for SpawnPipeline { .providers .get(&provider) .map(|p| p.api_base.clone()) - .unwrap_or_else(|| "https://opencode.ai/zen/v1".to_string()); + .unwrap_or_else(|| zesdex_domain::agent::defaults::DEFAULT_API_BASE.to_string()); let api_key = crate::llm::provider::resolve_api_key(&settings, &app_config); diff --git a/apps/infrastructure/src/tools/utility/todofinish.rs b/apps/infrastructure/src/tools/utility/todofinish.rs index 8b376f7..df92a77 100644 --- a/apps/infrastructure/src/tools/utility/todofinish.rs +++ b/apps/infrastructure/src/tools/utility/todofinish.rs @@ -68,8 +68,9 @@ impl Tool for Todofinish { content = new_lines.join("\n") + "\n"; let _ = std::fs::write(&todo_path, &content); if let Some(events) = &ctx.turn_events { - let mut q = events.lock().unwrap(); - q.push_back(crate::TurnEvent::TodoUpdate(content)); + if let Ok(mut q) = events.lock() { + q.push_back(crate::TurnEvent::TodoUpdate(content)); + } } info!(item, "TODO item completed and written back"); } else { diff --git a/apps/infrastructure/src/tools/utility/todowrite.rs b/apps/infrastructure/src/tools/utility/todowrite.rs index b45f477..d1888ec 100644 --- a/apps/infrastructure/src/tools/utility/todowrite.rs +++ b/apps/infrastructure/src/tools/utility/todowrite.rs @@ -62,8 +62,9 @@ impl Tool for Todowrite { let _ = std::fs::write(&todo_path, &content); if let Some(events) = &ctx.turn_events { - let mut q = events.lock().unwrap(); - q.push_back(crate::TurnEvent::TodoUpdate(content)); + if let Ok(mut q) = events.lock() { + q.push_back(crate::TurnEvent::TodoUpdate(content)); + } } info!(item, priority, "TODO item added"); diff --git a/apps/infrastructure/src/tools/workflow.rs b/apps/infrastructure/src/tools/workflow.rs index ffd3e2c..945d39d 100644 --- a/apps/infrastructure/src/tools/workflow.rs +++ b/apps/infrastructure/src/tools/workflow.rs @@ -62,7 +62,7 @@ impl Tool for WorkflowRun { let llm_client = LlmClient::new( crate::llm::provider::DEFAULT_API_KEY.to_string(), - "deepseek-v4-flash-free".to_string(), + zesdex_domain::agent::defaults::DEFAULT_MODEL.to_string(), None, ); let rt = tokio::runtime::Runtime::new()?; diff --git a/apps/infrastructure/src/workflow/engine/primitives.rs b/apps/infrastructure/src/workflow/engine/primitives.rs index 34f2092..4f8978c 100644 --- a/apps/infrastructure/src/workflow/engine/primitives.rs +++ b/apps/infrastructure/src/workflow/engine/primitives.rs @@ -41,7 +41,7 @@ pub async fn execute_primitive(directive: &str, tool_ctx: &ToolCtx) -> Result 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)?; diff --git a/apps/interfaces/tui/src/controller/input.rs b/apps/interfaces/tui/src/controller/input.rs index 88c83cc..f8d6226 100644 --- a/apps/interfaces/tui/src/controller/input.rs +++ b/apps/interfaces/tui/src/controller/input.rs @@ -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 { } } -/// 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 { - 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 = 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::*; diff --git a/apps/interfaces/tui/src/controller/mod.rs b/apps/interfaces/tui/src/controller/mod.rs index 0e6ced7..fa8dcde 100644 --- a/apps/interfaces/tui/src/controller/mod.rs +++ b/apps/interfaces/tui/src/controller/mod.rs @@ -9,3 +9,4 @@ //! commands into structured `Action` variants. pub mod command; pub mod input; +pub mod overlay_enter; diff --git a/apps/interfaces/tui/src/controller/overlay_enter.rs b/apps/interfaces/tui/src/controller/overlay_enter.rs new file mode 100644 index 0000000..6d7a7ab --- /dev/null +++ b/apps/interfaces/tui/src/controller/overlay_enter.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + let providers: Vec = 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 { + 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() +} diff --git a/apps/interfaces/tui/src/run.rs b/apps/interfaces/tui/src/run.rs index 0e51f88..a7e2668 100644 --- a/apps/interfaces/tui/src/run.rs +++ b/apps/interfaces/tui/src/run.rs @@ -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()?; diff --git a/apps/interfaces/tui/src/turn.rs b/apps/interfaces/tui/src/turn.rs index f272fb7..4f93652 100644 --- a/apps/interfaces/tui/src/turn.rs +++ b/apps/interfaces/tui/src/turn.rs @@ -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); + } +}