feat: centralize default constants and refactor overlay enter handling in TUI
This commit is contained in:
@@ -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;
|
||||||
@@ -6,6 +6,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use crate::core::{ChatMessage, ToolCallResult, UsageStats};
|
use crate::core::{ChatMessage, ToolCallResult, UsageStats};
|
||||||
|
|
||||||
|
pub mod defaults;
|
||||||
pub mod prompt;
|
pub mod prompt;
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ pub use error::DomainError;
|
|||||||
// Agent module top-level items (TurnEvent, SessionRuntime, etc.)
|
// Agent module top-level items (TurnEvent, SessionRuntime, etc.)
|
||||||
pub use agent::*;
|
pub use agent::*;
|
||||||
// Sub-module items need explicit re-exports
|
// Sub-module items need explicit re-exports
|
||||||
|
pub use agent::defaults::*;
|
||||||
pub use agent::progress::AgentProgress;
|
pub use agent::progress::AgentProgress;
|
||||||
pub use agent::prompt::{compaction_prompt, main_agent_prompt, subagent_directive};
|
pub use agent::prompt::{compaction_prompt, main_agent_prompt, subagent_directive};
|
||||||
pub use workflow::*;
|
pub use workflow::*;
|
||||||
|
|||||||
@@ -1,31 +1,5 @@
|
|||||||
//! Subagent domain models.
|
//! 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.
|
/// Access tier for subagent tool permissions.
|
||||||
///
|
///
|
||||||
/// Tiers are cumulative: `Write` includes everything in `Read`, and `Full`
|
/// Tiers are cumulative: `Write` includes everything in `Read`, and `Full`
|
||||||
|
|||||||
@@ -144,15 +144,15 @@ fn run_api_server(port: u16) -> anyhow::Result<()> {
|
|||||||
"JWT_SECRET environment variable not set; using insecure default. \
|
"JWT_SECRET environment variable not set; using insecure default. \
|
||||||
Set JWT_SECRET to a secure random value in production."
|
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(
|
let state = zesdex_api::ApiState::new(
|
||||||
store.base_dir.clone(),
|
store.base_dir.clone(),
|
||||||
jwt_secret,
|
jwt_secret,
|
||||||
"",
|
"",
|
||||||
"deepseek-v4-flash-free",
|
zesdex_domain::agent::defaults::DEFAULT_MODEL,
|
||||||
Some("https://opencode.ai/zen/v1".to_string()),
|
Some(zesdex_domain::agent::defaults::DEFAULT_API_BASE.to_string()),
|
||||||
);
|
);
|
||||||
let app = zesdex_api::build_router(state);
|
let app = zesdex_api::build_router(state);
|
||||||
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
|
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ pub mod lsp;
|
|||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
pub mod persistence;
|
pub mod persistence;
|
||||||
pub mod review;
|
|
||||||
pub mod subagent;
|
pub mod subagent;
|
||||||
pub mod tools;
|
pub mod tools;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ use zesdex_domain::core::{
|
|||||||
};
|
};
|
||||||
use zesdex_application::ports::ProviderService;
|
use zesdex_application::ports::ProviderService;
|
||||||
|
|
||||||
const DEFAULT_BASE_URL: &str = "https://opencode.ai/zen/v1";
|
use zesdex_domain::agent::defaults::{DEFAULT_API_BASE, DEFAULT_MODEL};
|
||||||
const DEFAULT_MODEL: &str = "deepseek-v4-flash-free";
|
const DEFAULT_BASE_URL: &str = DEFAULT_API_BASE;
|
||||||
pub const DEFAULT_API_KEY: &str = "";
|
pub const DEFAULT_API_KEY: &str = "";
|
||||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(600);
|
const REQUEST_TIMEOUT: Duration = Duration::from_secs(600);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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<PendingReview>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<PendingReview> {
|
|
||||||
self.entries.pop_front()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.entries.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.entries.len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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<usize>,
|
|
||||||
pub message: String,
|
|
||||||
pub suggestion: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
@@ -109,7 +109,7 @@ pub fn spawn_background_review(
|
|||||||
// 3. Resolve LLM credentials
|
// 3. Resolve LLM credentials
|
||||||
let base_url = api_base.unwrap_or_else(|| {
|
let base_url = api_base.unwrap_or_else(|| {
|
||||||
std::env::var("OPENAI_API_BASE")
|
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));
|
let client = LlmClient::new(api_key, model, Some(base_url));
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
//! Subagent event types — events emitted during subagent execution.
|
|
||||||
|
|
||||||
pub use zesdex_domain::subagent::SubagentEvent;
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -5,9 +5,5 @@ pub mod auto;
|
|||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod division;
|
pub mod division;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod event;
|
|
||||||
pub mod gating;
|
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
pub mod spawn;
|
pub mod spawn;
|
||||||
pub mod tools;
|
|
||||||
pub mod workspace;
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ impl SubagentProvider {
|
|||||||
///
|
///
|
||||||
/// Flow: reads `settings.provider` and `settings.model` → if model is empty,
|
/// Flow: reads `settings.provider` and `settings.model` → if model is empty,
|
||||||
/// falls back to the provider config's `default_model` → if that is also
|
/// 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]
|
#[instrument]
|
||||||
pub fn resolve_subagent_provider(
|
pub fn resolve_subagent_provider(
|
||||||
settings: &zesdex_domain::cms::Settings,
|
settings: &zesdex_domain::cms::Settings,
|
||||||
@@ -82,7 +82,7 @@ pub fn resolve_subagent_provider(
|
|||||||
.providers
|
.providers
|
||||||
.get(&provider)
|
.get(&provider)
|
||||||
.and_then(|p| p.default_model.clone())
|
.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 {
|
} else {
|
||||||
model
|
model
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<String> {
|
|
||||||
tool.run(ctx, args)
|
|
||||||
}
|
|
||||||
@@ -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<PathBuf> {
|
|
||||||
let ws = base_dir.join("subagent-workspaces").join(agent_id);
|
|
||||||
std::fs::create_dir_all(&ws)?;
|
|
||||||
Ok(ws)
|
|
||||||
}
|
|
||||||
@@ -100,7 +100,7 @@ impl Tool for ParallelDelegate {
|
|||||||
.providers
|
.providers
|
||||||
.get(&provider)
|
.get(&provider)
|
||||||
.map(|p| p.api_base.clone())
|
.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);
|
let api_key = crate::llm::provider::resolve_api_key(&settings, &app_config);
|
||||||
|
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ impl Tool for PlanEnter {
|
|||||||
let plan_path = ctx.session_dir.join("PLAN.md");
|
let plan_path = ctx.session_dir.join("PLAN.md");
|
||||||
let _ = std::fs::write(&plan_path, &plan_text);
|
let _ = std::fs::write(&plan_path, &plan_text);
|
||||||
if let Some(events) = &ctx.turn_events {
|
if let Some(events) = &ctx.turn_events {
|
||||||
let mut q = events.lock().unwrap();
|
if let Ok(mut q) = events.lock() {
|
||||||
q.push_back(crate::TurnEvent::PlanUpdate(plan_text.clone()));
|
q.push_back(crate::TurnEvent::PlanUpdate(plan_text.clone()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"Plan entered (length: {} chars). Waiting for approval...",
|
"Plan entered (length: {} chars). Waiting for approval...",
|
||||||
@@ -103,9 +104,10 @@ impl Tool for PlanReady {
|
|||||||
let plan_path = ctx.session_dir.join("PLAN.md");
|
let plan_path = ctx.session_dir.join("PLAN.md");
|
||||||
let _ = std::fs::write(&plan_path, &plan_content);
|
let _ = std::fs::write(&plan_path, &plan_content);
|
||||||
if let Some(events) = &ctx.turn_events {
|
if let Some(events) = &ctx.turn_events {
|
||||||
let mut q = events.lock().unwrap();
|
if let Ok(mut q) = events.lock() {
|
||||||
q.push_back(crate::TurnEvent::PlanUpdate(plan_content.clone()));
|
q.push_back(crate::TurnEvent::PlanUpdate(plan_content.clone()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(format!("Plan saved to {filename}. Starting execution."))
|
Ok(format!("Plan saved to {filename}. Starting execution."))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ impl Tool for SpawnAgents {
|
|||||||
.providers
|
.providers
|
||||||
.get(&provider)
|
.get(&provider)
|
||||||
.map(|p| p.api_base.clone())
|
.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);
|
let api_key = crate::llm::provider::resolve_api_key(&settings, &app_config);
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ impl Tool for SpawnPipeline {
|
|||||||
.providers
|
.providers
|
||||||
.get(&provider)
|
.get(&provider)
|
||||||
.map(|p| p.api_base.clone())
|
.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);
|
let api_key = crate::llm::provider::resolve_api_key(&settings, &app_config);
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,10 @@ impl Tool for Todofinish {
|
|||||||
content = new_lines.join("\n") + "\n";
|
content = new_lines.join("\n") + "\n";
|
||||||
let _ = std::fs::write(&todo_path, &content);
|
let _ = std::fs::write(&todo_path, &content);
|
||||||
if let Some(events) = &ctx.turn_events {
|
if let Some(events) = &ctx.turn_events {
|
||||||
let mut q = events.lock().unwrap();
|
if let Ok(mut q) = events.lock() {
|
||||||
q.push_back(crate::TurnEvent::TodoUpdate(content));
|
q.push_back(crate::TurnEvent::TodoUpdate(content));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
info!(item, "TODO item completed and written back");
|
info!(item, "TODO item completed and written back");
|
||||||
} else {
|
} else {
|
||||||
info!(item, "TODO item not found in TODO.md — nothing to mark");
|
info!(item, "TODO item not found in TODO.md — nothing to mark");
|
||||||
|
|||||||
@@ -62,9 +62,10 @@ impl Tool for Todowrite {
|
|||||||
let _ = std::fs::write(&todo_path, &content);
|
let _ = std::fs::write(&todo_path, &content);
|
||||||
|
|
||||||
if let Some(events) = &ctx.turn_events {
|
if let Some(events) = &ctx.turn_events {
|
||||||
let mut q = events.lock().unwrap();
|
if let Ok(mut q) = events.lock() {
|
||||||
q.push_back(crate::TurnEvent::TodoUpdate(content));
|
q.push_back(crate::TurnEvent::TodoUpdate(content));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
info!(item, priority, "TODO item added");
|
info!(item, priority, "TODO item added");
|
||||||
Ok(format!("[{}] TODO added: {}", priority, item))
|
Ok(format!("[{}] TODO added: {}", priority, item))
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ impl Tool for WorkflowRun {
|
|||||||
|
|
||||||
let llm_client = LlmClient::new(
|
let llm_client = LlmClient::new(
|
||||||
crate::llm::provider::DEFAULT_API_KEY.to_string(),
|
crate::llm::provider::DEFAULT_API_KEY.to_string(),
|
||||||
"deepseek-v4-flash-free".to_string(),
|
zesdex_domain::agent::defaults::DEFAULT_MODEL.to_string(),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
let rt = tokio::runtime::Runtime::new()?;
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub async fn execute_primitive(directive: &str, tool_ctx: &ToolCtx) -> Result<St
|
|||||||
.providers
|
.providers
|
||||||
.get(&provider)
|
.get(&provider)
|
||||||
.map(|p| p.api_base.clone())
|
.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);
|
let api_key = crate::llm::provider::resolve_api_key(&settings, &app_config);
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ pub async fn execute_cycle(
|
|||||||
.providers
|
.providers
|
||||||
.get(&provider)
|
.get(&provider)
|
||||||
.map(|p| p.api_base.clone())
|
.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);
|
let api_key = crate::llm::provider::resolve_api_key(&settings, &app_config);
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ use crate::state::create_session;
|
|||||||
/// save settings, and release the lock.
|
/// save settings, and release the lock.
|
||||||
pub fn run_daemon() -> Result<()> {
|
pub fn run_daemon() -> Result<()> {
|
||||||
tracing::info!("starting daemon process");
|
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");
|
let run_dir = store.base_dir.join("run");
|
||||||
std::fs::create_dir_all(&run_dir)?;
|
std::fs::create_dir_all(&run_dir)?;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|||||||
|
|
||||||
use crate::action::Action;
|
use crate::action::Action;
|
||||||
use crate::controller::command::{apply_command, parse_command};
|
use crate::controller::command::{apply_command, parse_command};
|
||||||
|
use crate::controller::overlay_enter::handle_overlay_enter;
|
||||||
use crate::state::{AutocompleteKind, Overlay, AppStateRest};
|
use crate::state::{AutocompleteKind, Overlay, AppStateRest};
|
||||||
|
|
||||||
/// Mark state dirty and return an empty action list.
|
/// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -9,3 +9,4 @@
|
|||||||
//! commands into structured `Action` variants.
|
//! commands into structured `Action` variants.
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod input;
|
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.
|
/// save settings.
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub fn run_single_process() -> Result<()> {
|
pub fn run_single_process() -> Result<()> {
|
||||||
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
let _guard = rt.enter();
|
||||||
|
|
||||||
// Create session state
|
// Create session state
|
||||||
info!("starting single-process TUI");
|
info!("starting single-process TUI");
|
||||||
let (_store, mut state) = create_local_session()?;
|
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;
|
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