From 7b0b53671f27f56e2117a86b3989522d8ad6bb11 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Thu, 27 Aug 2026 22:10:28 +0700 Subject: [PATCH] style: format seluruh workspace dengan cargo fmt Menyeragamkan format kode sesuai rustfmt (126 file). Sebelumnya lefthook pre-commit 'cargo fmt --check' akan gagal pada commit apa pun. --- apps/application/src/agent/mod.rs | 5 +- apps/application/src/agent/turn_service.rs | 41 +-- apps/application/src/auth/oauth_service.rs | 27 +- apps/application/src/auth/session_service.rs | 17 +- .../src/cms/conversation_service.rs | 6 +- apps/application/src/cms/settings_service.rs | 19 +- apps/application/src/lib.rs | 7 +- apps/application/src/ports/authentication.rs | 7 +- apps/domain/src/agent/mod.rs | 2 +- apps/domain/src/cms/conversation.rs | 2 +- apps/domain/src/cms/mod.rs | 2 +- apps/domain/src/cms/repository.rs | 6 +- apps/domain/src/cms/service.rs | 6 +- apps/domain/src/core/provider.rs | 21 +- apps/domain/src/core/store.rs | 8 +- apps/domain/src/lib.rs | 29 +- apps/gateway/src/bin/seed.rs | 9 +- apps/gateway/src/bin/test_load.rs | 4 +- apps/gateway/src/bin/test_parse.rs | 5 +- apps/gateway/src/main.rs | 6 +- .../infrastructure/src/auth/oauth_loopback.rs | 16 +- .../src/best_practice/arch_audit.rs | 43 ++- .../src/best_practice/code_quality.rs | 32 +- .../src/best_practice/commit.rs | 43 ++- .../src/best_practice/explore.rs | 24 +- apps/infrastructure/src/best_practice/mod.rs | 5 +- apps/infrastructure/src/bgbash/job.rs | 4 +- apps/infrastructure/src/guard/patterns.rs | 18 +- apps/infrastructure/src/ipc/protocol.rs | 5 +- apps/infrastructure/src/lib.rs | 3 +- apps/infrastructure/src/llm/provider.rs | 39 +- apps/infrastructure/src/lsp/client.rs | 12 +- apps/infrastructure/src/lsp/manager.rs | 1 - .../src/lsp/provisioner/discovery.rs | 10 +- .../src/lsp/provisioner/install.rs | 10 +- .../src/lsp/provisioner/manager.rs | 12 +- apps/infrastructure/src/mcp/manager.rs | 1 - apps/infrastructure/src/middleware/auth.rs | 6 +- apps/infrastructure/src/middleware/cors.rs | 26 -- apps/infrastructure/src/middleware/mod.rs | 1 - .../src/middleware/rate_limit.rs | 11 +- .../src/persistence/cms/app_config_repo.rs | 37 +- .../src/persistence/cms/memory_repo.rs | 9 +- .../src/persistence/cms/settings_repo.rs | 16 +- .../src/persistence/iam/session_lock_repo.rs | 4 +- apps/infrastructure/src/persistence/mod.rs | 16 +- .../src/subagent/auto/engine.rs | 15 +- apps/infrastructure/src/subagent/engine.rs | 26 +- apps/infrastructure/src/subagent/provider.rs | 4 +- .../infrastructure/src/tools/best_practice.rs | 28 +- apps/infrastructure/src/tools/context.rs | 6 +- apps/infrastructure/src/tools/executor.rs | 14 +- .../src/tools/git/git_operator.rs | 2 +- .../src/tools/git/git_worktree.rs | 13 +- apps/infrastructure/src/tools/graduated.rs | 6 +- .../src/tools/lsp/completion.rs | 13 +- apps/infrastructure/src/tools/lsp/connect.rs | 8 +- .../src/tools/lsp/definition.rs | 13 +- .../src/tools/lsp/diagnostics.rs | 11 +- .../src/tools/lsp/disconnect.rs | 2 +- apps/infrastructure/src/tools/lsp/hover.rs | 13 +- apps/infrastructure/src/tools/lsp/mod.rs | 8 +- .../src/tools/lsp/references.rs | 13 +- apps/infrastructure/src/tools/mod.rs | 2 +- .../src/tools/parallel_delegate.rs | 52 ++- apps/infrastructure/src/tools/search.rs | 6 +- .../src/tools/semantic_search.rs | 101 ++++-- apps/infrastructure/src/tools/shell.rs | 8 +- .../src/tools/shell_filter/credentials.rs | 6 +- .../src/tools/shell_filter/git.rs | 5 +- apps/infrastructure/src/tools/util.rs | 10 +- .../src/tools/utility/todofinish.rs | 21 +- .../src/tools/utility/todowrite.rs | 4 +- apps/infrastructure/src/tools/web_search.rs | 23 +- apps/infrastructure/src/tools/workflow.rs | 18 +- apps/infrastructure/src/utils.rs | 50 ++- .../src/workflow/hive_mind/cycle.rs | 5 +- .../src/workflow/hive_mind/live.rs | 1 - .../src/workflow/hive_mind/synthesis.rs | 5 +- apps/infrastructure/src/workflow/script.rs | 74 ++-- apps/interfaces/api/src/error.rs | 4 +- apps/interfaces/api/src/handlers/auth.rs | 12 +- apps/interfaces/api/src/handlers/chat.rs | 3 +- apps/interfaces/api/src/handlers/sessions.rs | 4 +- apps/interfaces/api/src/lib.rs | 12 +- apps/interfaces/api/src/middleware/auth.rs | 7 +- apps/interfaces/api/src/state.rs | 34 +- apps/interfaces/daemon/src/client.rs | 16 +- apps/interfaces/daemon/src/handler.rs | 87 +++-- apps/interfaces/daemon/src/state.rs | 19 +- apps/interfaces/tui/src/action/handlers.rs | 13 +- apps/interfaces/tui/src/action/mod.rs | 13 +- .../tui/src/components/component.rs | 6 +- apps/interfaces/tui/src/controller/command.rs | 24 +- apps/interfaces/tui/src/controller/input.rs | 20 +- .../tui/src/controller/overlay_enter.rs | 5 +- apps/interfaces/tui/src/lib.rs | 7 +- apps/interfaces/tui/src/run.rs | 29 +- apps/interfaces/tui/src/state/input.rs | 17 +- apps/interfaces/tui/src/state/misc.rs | 6 +- apps/interfaces/tui/src/state/mod.rs | 32 +- apps/interfaces/tui/src/turn.rs | 17 +- apps/interfaces/tui/src/view/chat.rs | 238 ++++++------ apps/interfaces/tui/src/view/input.rs | 8 +- apps/interfaces/tui/src/view/markdown.rs | 341 +++++++++--------- apps/interfaces/tui/src/view/overlays/bash.rs | 2 +- .../tui/src/view/overlays/clear_confirm.rs | 2 +- apps/interfaces/tui/src/view/overlays/diff.rs | 8 +- .../tui/src/view/overlays/editor.rs | 7 +- apps/interfaces/tui/src/view/overlays/help.rs | 7 +- .../tui/src/view/overlays/key_input.rs | 2 +- .../tui/src/view/overlays/learning.rs | 31 +- .../tui/src/view/overlays/loading.rs | 2 +- apps/interfaces/tui/src/view/overlays/mcp.rs | 7 +- apps/interfaces/tui/src/view/overlays/mod.rs | 21 +- .../tui/src/view/overlays/model_selector.rs | 5 +- apps/interfaces/tui/src/view/overlays/plan.rs | 2 +- .../tui/src/view/overlays/quit_confirm.rs | 2 +- .../tui/src/view/overlays/rewind.rs | 12 +- .../tui/src/view/overlays/settings.rs | 17 +- apps/interfaces/tui/src/view/overlays/todo.rs | 2 +- .../interfaces/tui/src/view/overlays/usage.rs | 18 +- apps/interfaces/tui/src/view/sidebar.rs | 28 +- apps/interfaces/tui/src/view/status.rs | 8 +- apps/interfaces/tui/src/view/workflow.rs | 6 +- apps/interfaces/web/src/lib.rs | 3 +- apps/interfaces/ws/src/lib.rs | 42 ++- 127 files changed, 1271 insertions(+), 1156 deletions(-) delete mode 100644 apps/infrastructure/src/middleware/cors.rs diff --git a/apps/application/src/agent/mod.rs b/apps/application/src/agent/mod.rs index 2bbf470..ece48c3 100644 --- a/apps/application/src/agent/mod.rs +++ b/apps/application/src/agent/mod.rs @@ -16,10 +16,7 @@ pub trait ToolExecutor: Send + Sync { /// Service for running agent turns asynchronously. pub trait AgentTurnService: Send + Sync { /// Run a full agent turn loop asynchronously. - fn run_turn( - &self, - params: AgentTurnParams, - ) -> impl Future> + Send; + fn run_turn(&self, params: AgentTurnParams) -> impl Future> + Send; } pub mod explore; diff --git a/apps/application/src/agent/turn_service.rs b/apps/application/src/agent/turn_service.rs index 4c09e28..ab42708 100644 --- a/apps/application/src/agent/turn_service.rs +++ b/apps/application/src/agent/turn_service.rs @@ -7,8 +7,8 @@ use zesdex_domain::agent::{AgentTurnParams, TurnEvent}; use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef}; use zesdex_domain::main_agent_prompt; -use crate::ports::ProviderService; use super::{ExploreService, ToolExecutor}; +use crate::ports::ProviderService; /// Maximum tool-call iterations per agent turn before forcing termination. const MAX_TURN_ITERATIONS: u32 = 50; @@ -124,11 +124,7 @@ pub struct AgentTurnServiceImpl { } impl AgentTurnServiceImpl { - pub fn new( - provider: Arc

, - tool_executor: Arc, - tool_defs: Vec, - ) -> Self { + pub fn new(provider: Arc

, tool_executor: Arc, tool_defs: Vec) -> Self { Self { provider, tool_executor, @@ -203,7 +199,10 @@ impl super::AgentTurnService for AgentTurnS }, ); - match explorer.explore(&user_query, &workspace_root, ¶ms.turn_events).await { + match explorer + .explore(&user_query, &workspace_root, ¶ms.turn_events) + .await + { Ok(output) => { // Insert each context message as a system message. // They go at index 0 and are removed after the turn @@ -236,7 +235,9 @@ impl super::AgentTurnService for AgentTurnS // entire turn, avoiding per-iteration clones of the full message list. // It is removed before emitting the Compacted event so persistence // does not store the prompt redundantly. - params.messages.insert(0, ChatMessage::system(main_agent_prompt())); + params + .messages + .insert(0, ChatMessage::system(main_agent_prompt())); let original_count = params.messages.len(); for iteration in 0..MAX_TURN_ITERATIONS { @@ -287,7 +288,8 @@ impl super::AgentTurnService for AgentTurnS // ── Execute each tool call ────────────────────────── for tc in &tool_calls { let output = - execute_tool_call(self.tool_executor.as_ref(), ¶ms.turn_events, tc).await; + execute_tool_call(self.tool_executor.as_ref(), ¶ms.turn_events, tc) + .await; params .messages .push(ChatMessage::tool(tc.id.clone(), output)); @@ -295,10 +297,7 @@ impl super::AgentTurnService for AgentTurnS } Err(e) => { warn!("{e}"); - push_event( - ¶ms.turn_events, - TurnEvent::Error(e), - ); + push_event(¶ms.turn_events, TurnEvent::Error(e)); break; } } @@ -307,10 +306,7 @@ impl super::AgentTurnService for AgentTurnS // Remove the synthetic sys_msg before shipping events to the TUI // so the transcript shows only the actual user/assistant/tool exchange. let compacted: Vec = params.messages.drain(original_count - 1..).collect(); - push_event( - ¶ms.turn_events, - TurnEvent::Compacted(compacted), - ); + push_event(¶ms.turn_events, TurnEvent::Compacted(compacted)); push_event(¶ms.turn_events, TurnEvent::Done); params.in_flight.store(false, Ordering::SeqCst); @@ -341,15 +337,16 @@ pub async fn compact_messages_with_ai( let split_idx = messages.len() - COMPACT_KEEP_TAIL; let evicted: Vec<_> = messages.drain(..split_idx).collect(); - let mut summary_prompt = vec![ - ChatMessage::system(zesdex_domain::compaction_prompt()), - ]; + let mut summary_prompt = vec![ChatMessage::system(zesdex_domain::compaction_prompt())]; summary_prompt.extend(evicted); summary_prompt.push(ChatMessage::user( "Please summarise our previous conversation above for context continuity.".to_string(), )); - match provider.chat(&summary_prompt, None, Some(1024), Some(0.3)).await { + match provider + .chat(&summary_prompt, None, Some(1024), Some(0.3)) + .await + { Ok((summary_msg, _)) => { let summary_text = summary_msg .content @@ -373,5 +370,3 @@ pub async fn compact_messages_with_ai( } } } - - diff --git a/apps/application/src/auth/oauth_service.rs b/apps/application/src/auth/oauth_service.rs index 454bd04..6e100f5 100644 --- a/apps/application/src/auth/oauth_service.rs +++ b/apps/application/src/auth/oauth_service.rs @@ -45,11 +45,7 @@ use sha2::{Digest, Sha256}; /// and clear them after a successful (or failed) flow completion. pub trait OAuthFlowStore: Send + Sync { /// Persist the PKCE code verifier and CSRF state token. - fn save_flow_state( - &self, - verifier: &str, - state: &str, - ) -> Result<(), ServiceError>; + fn save_flow_state(&self, verifier: &str, state: &str) -> Result<(), ServiceError>; /// Load the stored PKCE code verifier. fn load_verifier(&self) -> Result; @@ -141,12 +137,7 @@ pub struct OAuthUseCase { impl OAuthUseCase { /// Create a new OAuth use-case. - pub fn new( - token_repo: R, - flow_store: S, - token_exchanger: E, - token_path: PathBuf, - ) -> Self { + pub fn new(token_repo: R, flow_store: S, token_exchanger: E, token_path: PathBuf) -> Self { OAuthUseCase { token_repo, flow_store, @@ -156,8 +147,8 @@ impl OAuthUseCase - zesdex_domain::auth::OAuthService for OAuthUseCase +impl zesdex_domain::auth::OAuthService + for OAuthUseCase { fn start_flow( &self, @@ -183,13 +174,9 @@ impl "starting OAuth flow", ); - let mut url = url::Url::parse(&config.auth_url) - .map_err(|e| { - ServiceError::InvalidConfig(format!( - "invalid auth_url '{}': {e}", - config.auth_url - )) - })?; + let mut url = url::Url::parse(&config.auth_url).map_err(|e| { + ServiceError::InvalidConfig(format!("invalid auth_url '{}': {e}", config.auth_url)) + })?; url.query_pairs_mut() .append_pair("response_type", "code") diff --git a/apps/application/src/auth/session_service.rs b/apps/application/src/auth/session_service.rs index a41b487..0a4ab01 100644 --- a/apps/application/src/auth/session_service.rs +++ b/apps/application/src/auth/session_service.rs @@ -46,12 +46,11 @@ impl SessionServiceImpl { } } -impl - zesdex_domain::auth::SessionService for SessionServiceImpl +impl zesdex_domain::auth::SessionService + for SessionServiceImpl { fn create_session(&self, title: &str) -> Result { - let id = SessionId::new(&Uuid::new_v4().to_string()) - .map_err(ServiceError::Other)?; + let id = SessionId::new(&Uuid::new_v4().to_string()).map_err(ServiceError::Other)?; let title_owned = if title.is_empty() { "New Session".to_string() } else { @@ -59,8 +58,7 @@ impl }; let session = Session::new(id.into_string(), title_owned); tracing::debug!(session_id = %session.id, title = %session.title, "creating new session"); - self.session_repo - .save_session(&self.base_dir, &session)?; + self.session_repo.save_session(&self.base_dir, &session)?; Ok(session) } @@ -73,17 +71,14 @@ impl fn archive_session(&self, id: SessionId) -> Result<(), ServiceError> { tracing::debug!(session_id = %id, "archiving session"); - let mut session = self - .session_repo - .load_session(&self.base_dir, &id)?; + let mut session = self.session_repo.load_session(&self.base_dir, &id)?; session.archived = true; let millis = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_millis(); session.updated_at = i64::try_from(millis).unwrap_or(i64::MAX); - self.session_repo - .save_session(&self.base_dir, &session)?; + self.session_repo.save_session(&self.base_dir, &session)?; Ok(()) } } diff --git a/apps/application/src/cms/conversation_service.rs b/apps/application/src/cms/conversation_service.rs index 0f82ff5..1896a72 100644 --- a/apps/application/src/cms/conversation_service.rs +++ b/apps/application/src/cms/conversation_service.rs @@ -58,11 +58,7 @@ impl zesdex_domain::cms::ConversationService Ok(()) } - fn add_message( - &self, - conv: &mut Conversation, - msg: ChatMessage, - ) -> Result<(), ServiceError> { + fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError> { tracing::debug!("adding message to session {}", conv.session_id); conv.push(msg); let dir = self.session_dir(&conv.session_id); diff --git a/apps/application/src/cms/settings_service.rs b/apps/application/src/cms/settings_service.rs index 598430b..1144dbd 100644 --- a/apps/application/src/cms/settings_service.rs +++ b/apps/application/src/cms/settings_service.rs @@ -14,8 +14,7 @@ use std::path::PathBuf; use tracing; use zesdex_domain::cms::{ - AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings, - SettingsRepository, + AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings, SettingsRepository, }; /// Service implementation for settings and app-config operations. @@ -30,11 +29,7 @@ pub struct SettingsServiceImpl { impl SettingsServiceImpl { /// Create a new service with the given repositories and base directory. - pub fn new( - settings_repo: S, - app_config_repo: C, - base_dir: impl Into, - ) -> Self { + pub fn new(settings_repo: S, app_config_repo: C, base_dir: impl Into) -> Self { tracing::debug!("creating SettingsServiceImpl"); Self { settings_repo, @@ -44,8 +39,8 @@ impl SettingsServiceImpl { } } -impl - zesdex_domain::cms::SettingsService for SettingsServiceImpl +impl zesdex_domain::cms::SettingsService + for SettingsServiceImpl { fn load_settings(&self) -> Result { tracing::debug!("loading settings"); @@ -60,11 +55,7 @@ impl Ok(()) } - fn update_provider( - &self, - name: &str, - config: &ProviderConfig, - ) -> Result<(), ServiceError> { + fn update_provider(&self, name: &str, config: &ProviderConfig) -> Result<(), ServiceError> { tracing::debug!("updating provider '{name}'"); let mut app_config: AppConfig = self.app_config_repo.load(&self.base_dir)?; app_config diff --git a/apps/application/src/lib.rs b/apps/application/src/lib.rs index b7b29c2..5b9b767 100644 --- a/apps/application/src/lib.rs +++ b/apps/application/src/lib.rs @@ -30,10 +30,10 @@ //! the use-case logic independent of any specific persistence or infrastructure //! technology. +pub mod agent; pub mod auth; pub mod cms; pub mod ports; -pub mod agent; // Re-export port traits for ergonomic access. pub use ports::*; @@ -46,12 +46,11 @@ pub use auth::{ // Re-export CMS use-cases. pub use cms::{ - conversation_service::ConversationServiceImpl, - memory_service::MemoryServiceImpl, + conversation_service::ConversationServiceImpl, memory_service::MemoryServiceImpl, settings_service::SettingsServiceImpl, }; pub use agent::{ + turn_service::{compact_messages_with_ai, AgentTurnServiceImpl}, AgentTurnService, ExploreOutput, ExploreService, ToolExecutor, - turn_service::{AgentTurnServiceImpl, compact_messages_with_ai}, }; diff --git a/apps/application/src/ports/authentication.rs b/apps/application/src/ports/authentication.rs index 55db32b..e628236 100644 --- a/apps/application/src/ports/authentication.rs +++ b/apps/application/src/ports/authentication.rs @@ -22,11 +22,8 @@ pub trait AuthService: Send + Sync { /// Authenticate a user by verifying a password against a stored hash. /// /// Returns `true` if the password matches, `false` otherwise. - fn authenticate( - &self, - password: &str, - hash: &str, - ) -> impl Future> + Send; + fn authenticate(&self, password: &str, hash: &str) + -> impl Future> + Send; /// Issue a new access + refresh token pair for the given subject. /// diff --git a/apps/domain/src/agent/mod.rs b/apps/domain/src/agent/mod.rs index dae7e30..4738ec3 100644 --- a/apps/domain/src/agent/mod.rs +++ b/apps/domain/src/agent/mod.rs @@ -7,8 +7,8 @@ use std::path::PathBuf; use crate::core::{ChatMessage, ToolCallResult, UsageStats}; pub mod defaults; -pub mod prompt; pub mod progress; +pub mod prompt; /// Which kind of caller (main agent vs. subagent vs. reviewer) is /// invoking a tool, used to scope permissions and tag log/output paths. diff --git a/apps/domain/src/cms/conversation.rs b/apps/domain/src/cms/conversation.rs index c468087..3c0728a 100644 --- a/apps/domain/src/cms/conversation.rs +++ b/apps/domain/src/cms/conversation.rs @@ -11,5 +11,5 @@ //! - `ChatMessage` — a single message with role, content, and tool metadata //! - `Role` — message role enum (User, Assistant, System, Tool) -pub use crate::core::message::{ChatMessage, Role}; pub use crate::core::conversation::Conversation; +pub use crate::core::message::{ChatMessage, Role}; diff --git a/apps/domain/src/cms/mod.rs b/apps/domain/src/cms/mod.rs index 67bcacc..9ce6fc6 100644 --- a/apps/domain/src/cms/mod.rs +++ b/apps/domain/src/cms/mod.rs @@ -32,6 +32,7 @@ pub mod settings; pub use app_config::AppConfig; pub use app_config::ModelRole; pub use app_config::ProviderConfig; +pub use commands::{NewMemory, SettingsPatch}; pub use conversation::Conversation; pub use edit_log::EditLog; pub use edit_log::EditLogEntry; @@ -46,7 +47,6 @@ pub use repository::SettingsRepository; pub use service::ConversationService; pub use service::MemoryService; pub use service::SettingsService; -pub use commands::{NewMemory, SettingsPatch}; pub use settings::InternetMode; pub use settings::Settings; pub use settings::SettingsFlags; diff --git a/apps/domain/src/cms/repository.rs b/apps/domain/src/cms/repository.rs index 09149bf..7ecee7e 100644 --- a/apps/domain/src/cms/repository.rs +++ b/apps/domain/src/cms/repository.rs @@ -56,11 +56,7 @@ pub trait ConversationRepository { fn load(&self, session_dir: &Path) -> Result; /// Persist a `Conversation` to the given session directory. - fn save( - &self, - session_dir: &Path, - conversation: &Conversation, - ) -> Result<(), RepositoryError>; + fn save(&self, session_dir: &Path, conversation: &Conversation) -> Result<(), RepositoryError>; } /// Persistence contract for `Memory` (long-term agent memory entries). diff --git a/apps/domain/src/cms/service.rs b/apps/domain/src/cms/service.rs index de9fc40..cf451d9 100644 --- a/apps/domain/src/cms/service.rs +++ b/apps/domain/src/cms/service.rs @@ -44,11 +44,7 @@ pub trait ConversationService { fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError>; /// Append a single `ChatMessage` to the conversation and persist. - fn add_message( - &self, - conv: &mut Conversation, - msg: ChatMessage, - ) -> Result<(), ServiceError>; + fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError>; } /// Use-cases for long-term memory management. diff --git a/apps/domain/src/core/provider.rs b/apps/domain/src/core/provider.rs index 296bb29..963bc74 100644 --- a/apps/domain/src/core/provider.rs +++ b/apps/domain/src/core/provider.rs @@ -335,16 +335,16 @@ impl SseParser { { const MAX_TOOL_CALLS: usize = 64; for tc in tool_calls { - let raw_index = - tc.get("index").and_then(Value::as_u64).unwrap_or_else( - || { - tracing::warn!( - "[stream] tool call delta missing index, \ + let raw_index = tc + .get("index") + .and_then(Value::as_u64) + .unwrap_or_else(|| { + tracing::warn!( + "[stream] tool call delta missing index, \ defaulting to 0" - ); - 0 - }, - ); + ); + 0 + }); // Clamp index to prevent out-of-bounds / memory exhaustion let index = usize::try_from(raw_index) .unwrap_or(0) @@ -395,8 +395,7 @@ impl SseParser { if let Some(content) = delta.get("text").and_then(|c| c.as_str()) { d_events.push(StreamEvent::Token(content.to_string())); } - if let Some(reasoning) = - delta.get("reasoning_content").and_then(|r| r.as_str()) + if let Some(reasoning) = delta.get("reasoning_content").and_then(|r| r.as_str()) { d_events.push(StreamEvent::Reasoning(reasoning.to_string())); } diff --git a/apps/domain/src/core/store.rs b/apps/domain/src/core/store.rs index 3eb106d..1d2ef38 100644 --- a/apps/domain/src/core/store.rs +++ b/apps/domain/src/core/store.rs @@ -40,9 +40,11 @@ impl Store { /// /// Why: paths are computed, not created — call `ensure_dirs` before use. pub fn new() -> Self { - let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok() - .or_else(|| std::env::var("HOME").ok().map(|h| format!("{h}/.local/share"))) - { + let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok().or_else(|| { + std::env::var("HOME") + .ok() + .map(|h| format!("{h}/.local/share")) + }) { PathBuf::from(data_dir).join("zesdex") } else { PathBuf::from(".local/share/zesdex") diff --git a/apps/domain/src/lib.rs b/apps/domain/src/lib.rs index a26bd25..6a8e4a9 100644 --- a/apps/domain/src/lib.rs +++ b/apps/domain/src/lib.rs @@ -24,33 +24,32 @@ //! no framework imports, no side effects. All persistence is expressed //! through repository traits that infrastructure adapters implement. +pub mod agent; pub mod auth; pub mod cms; pub mod core; pub mod error; -pub mod agent; -pub mod workflow; pub mod subagent; +pub mod workflow; // Re-export all public items from each module for ergonomic imports. // Consumers can do `use zesdex_domain::*` for common types. pub use auth::{ - IamSession, NewSession, OAuthConfig, OAuthToken, OAuthRepository, OAuthService, - RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session, - SessionId, SessionLock, SessionLockRepository, SessionRepository, SessionService, + IamSession, NewSession, OAuthConfig, OAuthRepository, OAuthService, OAuthToken, + RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session, SessionId, + SessionLock, SessionLockRepository, SessionRepository, SessionService, }; pub use cms::{ - AppConfig, AppConfigRepository, Conversation as CmsConversation, - ConversationRepository, ConversationService, EditLog, EditLogEntry, - EditLogRepository, InternetMode, Memory, MemoryRepository, MemoryService, - ModelRole, NewMemory, ProviderConfig, RepositoryError as CmsRepositoryError, - ServiceError as CmsServiceError, Settings, SettingsFlags, SettingsPatch, - SettingsRepository, SettingsService, + AppConfig, AppConfigRepository, Conversation as CmsConversation, ConversationRepository, + ConversationService, EditLog, EditLogEntry, EditLogRepository, InternetMode, Memory, + MemoryRepository, MemoryService, ModelRole, NewMemory, ProviderConfig, + RepositoryError as CmsRepositoryError, ServiceError as CmsServiceError, Settings, + SettingsFlags, SettingsPatch, SettingsRepository, SettingsService, }; pub use core::{ - ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role, - SseParser, StreamEvent, StreamOptions, Store, TokenUsage, ToolCall, - ToolCallResult, ToolDef, ToolFunction, ToolFunctionDef, UsageStats, + ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role, SseParser, Store, + StreamEvent, StreamOptions, TokenUsage, ToolCall, ToolCallResult, ToolDef, ToolFunction, + ToolFunctionDef, UsageStats, }; pub use error::DomainError; @@ -60,5 +59,5 @@ pub use agent::*; pub use agent::defaults::*; pub use agent::progress::AgentProgress; pub use agent::prompt::{compaction_prompt, main_agent_prompt, subagent_directive}; -pub use workflow::*; pub use subagent::*; +pub use workflow::*; diff --git a/apps/gateway/src/bin/seed.rs b/apps/gateway/src/bin/seed.rs index fd4636b..cd65ef5 100644 --- a/apps/gateway/src/bin/seed.rs +++ b/apps/gateway/src/bin/seed.rs @@ -4,7 +4,6 @@ //! configuration files plus a seed session for development/testing. //! Invoked as `cargo run --bin seed`. - fn main() -> anyhow::Result<()> { let store = zesdex_domain::core::Store::new(); store.ensure_dirs()?; @@ -46,13 +45,11 @@ fn main() -> anyhow::Result<()> { // Create a seed session let session_id = uuid::Uuid::new_v4().to_string(); - let session = zesdex_domain::auth::Session::new( - session_id.clone(), - "Seed Session".to_string(), - ); + let session = zesdex_domain::auth::Session::new(session_id.clone(), "Seed Session".to_string()); // Persist via the session repository use zesdex_domain::SessionRepository; - let repo = zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new(); + let repo = + zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new(); repo.save_session(&store.base_dir, &session)?; tracing::info!("Seed session created: id={session_id}"); diff --git a/apps/gateway/src/bin/test_load.rs b/apps/gateway/src/bin/test_load.rs index 32acd13..ddac4c8 100644 --- a/apps/gateway/src/bin/test_load.rs +++ b/apps/gateway/src/bin/test_load.rs @@ -5,12 +5,12 @@ fn main() { let base_dir = dirs::home_dir().unwrap().join(".local/share/zesdex"); let repo = JsonAppConfigRepository::new(); let config = repo.load(&base_dir).unwrap(); - + println!("Providers:"); for (k, v) in &config.providers { println!(" - {} (default model: {:?})", k, v.default_model); } - + println!("Default provider: {}", config.default_provider); println!("Default model: {}", config.default_model); println!("Model roles:"); diff --git a/apps/gateway/src/bin/test_parse.rs b/apps/gateway/src/bin/test_parse.rs index 092852b..865eb58 100644 --- a/apps/gateway/src/bin/test_parse.rs +++ b/apps/gateway/src/bin/test_parse.rs @@ -16,7 +16,10 @@ struct ClaudeSettings { } fn main() { - let path = dirs::home_dir().unwrap().join(".claude").join("settings.json"); + let path = dirs::home_dir() + .unwrap() + .join(".claude") + .join("settings.json"); println!("Path: {:?}", path); match std::fs::read_to_string(&path) { Ok(content) => { diff --git a/apps/gateway/src/main.rs b/apps/gateway/src/main.rs index c2648d0..25717fd 100644 --- a/apps/gateway/src/main.rs +++ b/apps/gateway/src/main.rs @@ -12,7 +12,11 @@ use clap::Parser; /// Zesdex — autonomous AI coding agent. #[derive(Parser, Debug)] -#[command(name = "zesdex", version, about = "Autonomous AI coding agent with TUI")] +#[command( + name = "zesdex", + version, + about = "Autonomous AI coding agent with TUI" +)] struct Cli { /// Run as background daemon with IPC socket #[arg(long)] diff --git a/apps/infrastructure/src/auth/oauth_loopback.rs b/apps/infrastructure/src/auth/oauth_loopback.rs index 4e309ae..0e579bd 100644 --- a/apps/infrastructure/src/auth/oauth_loopback.rs +++ b/apps/infrastructure/src/auth/oauth_loopback.rs @@ -21,20 +21,13 @@ impl LoopbackServer { format!("http://127.0.0.1:{}/callback", self.port) } - pub fn wait_for_code( - &self, - timeout_ms: u64, - expected_state: &str, - ) -> std::io::Result { + pub fn wait_for_code(&self, timeout_ms: u64, expected_state: &str) -> std::io::Result { let (mut stream, _) = self.listener.accept()?; stream.set_read_timeout(Some(std::time::Duration::from_millis(timeout_ms)))?; Self::read_callback(&mut stream, expected_state) } - fn read_callback( - stream: &mut TcpStream, - expected_state: &str, - ) -> std::io::Result { + fn read_callback(stream: &mut TcpStream, expected_state: &str) -> std::io::Result { let mut buf = [0u8; 4096]; let n = stream.read(&mut buf)?; let request = String::from_utf8_lossy(&buf[..n]); @@ -68,7 +61,10 @@ impl LoopbackServer { )); } code.ok_or_else(|| { - std::io::Error::new(std::io::ErrorKind::InvalidData, "code not found in callback") + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "code not found in callback", + ) }) } diff --git a/apps/infrastructure/src/best_practice/arch_audit.rs b/apps/infrastructure/src/best_practice/arch_audit.rs index cd43418..7965f7c 100644 --- a/apps/infrastructure/src/best_practice/arch_audit.rs +++ b/apps/infrastructure/src/best_practice/arch_audit.rs @@ -59,17 +59,25 @@ pub struct AuditReport { impl AuditReport { /// True if any ERROR-level violations exist. pub fn has_errors(&self) -> bool { - self.violations.iter().any(|v| v.severity == Severity::Error) + self.violations + .iter() + .any(|v| v.severity == Severity::Error) } /// Number of errors. pub fn error_count(&self) -> usize { - self.violations.iter().filter(|v| v.severity == Severity::Error).count() + self.violations + .iter() + .filter(|v| v.severity == Severity::Error) + .count() } /// Number of warnings. pub fn warning_count(&self) -> usize { - self.violations.iter().filter(|v| v.severity == Severity::Warning).count() + self.violations + .iter() + .filter(|v| v.severity == Severity::Warning) + .count() } } @@ -152,11 +160,7 @@ fn forbidden_imports(layer: &str) -> &'static [&'static str] { } /// Scan a single Rust source file for forbidden imports. -fn scan_file( - file_path: &Path, - layer: &'static str, - root: &Path, -) -> Vec { +fn scan_file(file_path: &Path, layer: &'static str, root: &Path) -> Vec { let mut violations = Vec::new(); let content = match std::fs::read_to_string(file_path) { Ok(c) => c, @@ -185,10 +189,12 @@ fn scan_file( // `use crate::` in domain could reference domain-only items — skip. continue; } - if trimmed.starts_with(&pattern) || trimmed.starts_with(&format!("use {forbidden}::")) { + if trimmed.starts_with(&pattern) + || trimmed.starts_with(&format!("use {forbidden}::")) + { // Skip test code — test modules commonly import outer layers. - let is_test = content[..content.len().saturating_sub(1)] - .contains("#[cfg(test)]"); + let is_test = + content[..content.len().saturating_sub(1)].contains("#[cfg(test)]"); if is_test { continue; } @@ -243,10 +249,7 @@ pub fn audit_layering(root: &Path) -> Result { } // Determine which crate this file belongs to by walking up. - let layer = path - .ancestors() - .skip(1) - .find_map(|p| classify_layer(p)); + let layer = path.ancestors().skip(1).find_map(|p| classify_layer(p)); if let Some(layer) = layer { files_scanned += 1; @@ -371,7 +374,10 @@ mod tests { fn function_metrics_short_function_ok() { let content = "fn ok() {\n let x = 1;\n}\n"; let violations = check_function_metrics(content); - let long: Vec<_> = violations.iter().filter(|v| v.message.contains("Function too long")).collect(); + let long: Vec<_> = violations + .iter() + .filter(|v| v.message.contains("Function too long")) + .collect(); assert!(long.is_empty(), "short function should not trigger"); } @@ -383,7 +389,10 @@ mod tests { } lines.push_str("}\n"); let violations = check_function_metrics(&lines); - let long: Vec<_> = violations.iter().filter(|v| v.message.contains("Function too long")).collect(); + let long: Vec<_> = violations + .iter() + .filter(|v| v.message.contains("Function too long")) + .collect(); assert!(!long.is_empty(), "long function should trigger warning"); } } diff --git a/apps/infrastructure/src/best_practice/code_quality.rs b/apps/infrastructure/src/best_practice/code_quality.rs index cc33653..e6d9002 100644 --- a/apps/infrastructure/src/best_practice/code_quality.rs +++ b/apps/infrastructure/src/best_practice/code_quality.rs @@ -36,7 +36,9 @@ pub struct CodeQualityReport { impl CodeQualityReport { pub fn has_errors(&self) -> bool { - self.findings.iter().any(|f| f.severity == super::arch_audit::Severity::Error) + self.findings + .iter() + .any(|f| f.severity == super::arch_audit::Severity::Error) } pub fn count_by_rule(&self) -> Vec<(&'static str, usize)> { let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new(); @@ -147,7 +149,8 @@ pub fn scan_quality_file(file_path: &Path, root: &Path) -> Vec { // ── Rule: Missing doc comments on pub items ──────────────────── if (trimmed.starts_with("pub ") || trimmed.starts_with("pub(")) - && !prev_line_doc && !prev_line_empty + && !prev_line_doc + && !prev_line_empty { // Check it's a struct/enum/fn/trait/type/const/mod let is_item = trimmed.starts_with("pub fn ") @@ -246,7 +249,10 @@ mod tests { std::fs::write(&file, "fn x() { let y = foo.unwrap(); }\n").unwrap(); let findings = scan_quality_file(&file, &dir); - let unwrap_findings: Vec<_> = findings.iter().filter(|f| f.rule == "unwrap-in-production").collect(); + let unwrap_findings: Vec<_> = findings + .iter() + .filter(|f| f.rule == "unwrap-in-production") + .collect(); assert!(!unwrap_findings.is_empty(), "should detect unwrap"); } @@ -262,8 +268,14 @@ mod tests { .unwrap(); let findings = scan_quality_file(&file, &dir); - let unwrap_findings: Vec<_> = findings.iter().filter(|f| f.rule == "unwrap-in-production").collect(); - assert!(unwrap_findings.is_empty(), "should skip unwrap in test blocks"); + let unwrap_findings: Vec<_> = findings + .iter() + .filter(|f| f.rule == "unwrap-in-production") + .collect(); + assert!( + unwrap_findings.is_empty(), + "should skip unwrap in test blocks" + ); } #[test] @@ -274,7 +286,13 @@ mod tests { std::fs::write(&file, "#[allow(clippy::too_many_arguments)]\nfn x() {}\n").unwrap(); let findings = scan_quality_file(&file, &dir); - let bypass_findings: Vec<_> = findings.iter().filter(|f| f.rule == "compiler-bypass").collect(); - assert!(!bypass_findings.is_empty(), "should detect allow attributes"); + let bypass_findings: Vec<_> = findings + .iter() + .filter(|f| f.rule == "compiler-bypass") + .collect(); + assert!( + !bypass_findings.is_empty(), + "should detect allow attributes" + ); } } diff --git a/apps/infrastructure/src/best_practice/commit.rs b/apps/infrastructure/src/best_practice/commit.rs index cf90900..57a84ea 100644 --- a/apps/infrastructure/src/best_practice/commit.rs +++ b/apps/infrastructure/src/best_practice/commit.rs @@ -50,10 +50,9 @@ pub fn validate_commit_message(msg: &str) -> Result<(), Vec> { } // Parse: `type(scope): description` or `type!: description` or `type: description` - let re = Regex::new( - r"^(?P[a-z]+)(?:\((?P[^)]+)\))?(?P!)?:\s+(?P.+)$", - ) - .expect("valid regex for commit parsing"); + let re = + Regex::new(r"^(?P[a-z]+)(?:\((?P[^)]+)\))?(?P!)?:\s+(?P.+)$") + .expect("valid regex for commit parsing"); match re.captures(subject) { None => { @@ -94,21 +93,19 @@ pub fn validate_commit_message(msg: &str) -> Result<(), Vec> { } else { let first_char = desc.chars().next().unwrap_or(' '); if first_char.is_uppercase() { - errors.push(format!( - "Description must start with lowercase: '{desc}'" - )); + errors.push(format!("Description must start with lowercase: '{desc}'")); } if desc.ends_with('.') { - errors.push(format!( - "Description must not end with a period: '{desc}'" - )); + errors.push(format!("Description must not end with a period: '{desc}'")); } } // Type-specific rules. match type_ { "chore" | "docs" | "refactor" | "test" | "style" | "perf" | "ci" | "build" - | "revert" if scope.is_some() => { + | "revert" + if scope.is_some() => + { errors.push(format!( "'{type_}' commits should not use a scope. \ Only 'feat' and 'fix' require scopes." @@ -140,10 +137,9 @@ pub fn parse_commit_message(msg: &str) -> Option { let msg = msg.trim(); let subject = msg.lines().next()?; - let re = Regex::new( - r"^(?P[a-z]+)(?:\((?P[^)]+)\))?(?P!)?:\s+(?P.+)$", - ) - .expect("valid regex"); + let re = + Regex::new(r"^(?P[a-z]+)(?:\((?P[^)]+)\))?(?P!)?:\s+(?P.+)$") + .expect("valid regex"); let caps = re.captures(subject)?; @@ -155,10 +151,18 @@ pub fn parse_commit_message(msg: &str) -> Option { }; Some(CommitInfo { - type_: caps.name("type").map(|m| m.as_str()).unwrap_or("").to_string(), + type_: caps + .name("type") + .map(|m| m.as_str()) + .unwrap_or("") + .to_string(), scope: caps.name("scope").map(|m| m.as_str().to_string()), breaking: caps.name("breaking").is_some(), - description: caps.name("desc").map(|m| m.as_str()).unwrap_or("").to_string(), + description: caps + .name("desc") + .map(|m| m.as_str()) + .unwrap_or("") + .to_string(), body, }) } @@ -232,7 +236,10 @@ mod tests { #[test] fn parses_valid_commit() { - let parsed = parse_commit_message("feat(agent): add parallel execution\n\nWith cycle orchestration.").unwrap(); + let parsed = parse_commit_message( + "feat(agent): add parallel execution\n\nWith cycle orchestration.", + ) + .unwrap(); assert_eq!(parsed.type_, "feat"); assert_eq!(parsed.scope, Some("agent".to_string())); assert!(!parsed.breaking); diff --git a/apps/infrastructure/src/best_practice/explore.rs b/apps/infrastructure/src/best_practice/explore.rs index c9888d5..9c92686 100644 --- a/apps/infrastructure/src/best_practice/explore.rs +++ b/apps/infrastructure/src/best_practice/explore.rs @@ -51,7 +51,6 @@ const EXPLORE_DIRECTIVES: [&str; 3] = [ 4. Identify main entry points (main.rs, main.py, index.ts, etc.).\n\ 5. Count files by extension type.\n\ Use the ls_dir, read, grep, and glob tools. Be concise.", - // Agent 1: Symbol Index "You are a symbol index explorer.\n\ 1. Call the 'rebuild_index' tool to rebuild the symbol index.\n\ @@ -59,7 +58,6 @@ const EXPLORE_DIRECTIVES: [&str; 3] = [ 3. Identify public APIs, entry points, and key types.\n\ 4. Group symbols by language and kind.\n\ Be concise. Report what symbols exist and where they live.", - // Agent 2: Semantic Context "You are a semantic context explorer.\n\ 1. Call the 'rebuild_index' tool to ensure the index is fresh.\n\ @@ -231,8 +229,8 @@ async fn run_explore_phase( let tc = tool_ctx.clone(); let handle = thread::spawn(move || { - let rt = tokio::runtime::Runtime::new() - .context("create explore subagent tokio runtime")?; + let rt = + tokio::runtime::Runtime::new().context("create explore subagent tokio runtime")?; rt.block_on(run_agent(ctx, &directive, AccessTier::Read, tc)) }); @@ -252,12 +250,22 @@ async fn run_explore_phase( } Ok(Err(e)) => { warn!(agent = i, error = %e, "explore subagent failed"); - emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], &e.to_string()); + emit_failed( + &turn_events_clone, + EXPLORE_IDS[i], + EXPLORE_LABELS[i], + &e.to_string(), + ); (i, format!("Error: {e}"), false) } Err(e) => { warn!(agent = i, error = ?e, "explore subagent panicked"); - emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], "thread panicked"); + emit_failed( + &turn_events_clone, + EXPLORE_IDS[i], + EXPLORE_LABELS[i], + "thread panicked", + ); (i, format!("Thread panic: {e:?}"), false) } }; @@ -281,9 +289,7 @@ fn build_explore_context(results: &[(usize, String, bool)]) -> String { let success_count = results.iter().filter(|r| r.2).count(); let total = results.len(); - let mut msg = format!( - "[Explore Phase — {success_count}/{total} agents succeeded]\n\n" - ); + let mut msg = format!("[Explore Phase — {success_count}/{total} agents succeeded]\n\n"); for (i, output, success) in results { let label = EXPLORE_LABELS.get(*i).unwrap_or(&"❓ Unknown"); diff --git a/apps/infrastructure/src/best_practice/mod.rs b/apps/infrastructure/src/best_practice/mod.rs index a2b4bfa..7e105fd 100644 --- a/apps/infrastructure/src/best_practice/mod.rs +++ b/apps/infrastructure/src/best_practice/mod.rs @@ -107,10 +107,7 @@ impl BestPracticeEngine { let layering = self.audit_layering(workspace_root)?; let quality = self.scan_quality(workspace_root)?; - Ok(CombinedAuditReport { - layering, - quality, - }) + Ok(CombinedAuditReport { layering, quality }) } } diff --git a/apps/infrastructure/src/bgbash/job.rs b/apps/infrastructure/src/bgbash/job.rs index a77aac0..79daf4d 100644 --- a/apps/infrastructure/src/bgbash/job.rs +++ b/apps/infrastructure/src/bgbash/job.rs @@ -89,6 +89,8 @@ impl BashJob { let Ok(mut guard) = self.process.lock() else { return false; }; - guard.as_mut().is_some_and(|c| matches!(c.try_wait(), Ok(None))) + guard + .as_mut() + .is_some_and(|c| matches!(c.try_wait(), Ok(None))) } } diff --git a/apps/infrastructure/src/guard/patterns.rs b/apps/infrastructure/src/guard/patterns.rs index 13bed86..e43e53e 100644 --- a/apps/infrastructure/src/guard/patterns.rs +++ b/apps/infrastructure/src/guard/patterns.rs @@ -7,24 +7,22 @@ pub fn check_dangerous_pattern(tool_name: &str, args: &serde_json::Value) -> Option { match tool_name { "bash" => { - let cmd = args - .get("command") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or(""); // Detect git push with --force if cmd.contains("git push") && cmd.contains("--force") { - return Some("Force-pushing to git is destructive and may lose history".to_string()); + return Some( + "Force-pushing to git is destructive and may lose history".to_string(), + ); } // Detect rm -rf / if cmd.contains("rm -rf /") || cmd.contains("rm -rf /*") { - return Some("Recursive deletion of the root filesystem is never allowed".to_string()); + return Some( + "Recursive deletion of the root filesystem is never allowed".to_string(), + ); } } "delete" => { - let path = args - .get("path") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(""); if path == "/" || path.starts_with("/etc") { return Some(format!("Deleting '{}' is too dangerous", path)); } diff --git a/apps/infrastructure/src/ipc/protocol.rs b/apps/infrastructure/src/ipc/protocol.rs index 878eef5..6eb4829 100644 --- a/apps/infrastructure/src/ipc/protocol.rs +++ b/apps/infrastructure/src/ipc/protocol.rs @@ -76,10 +76,7 @@ pub struct StatePayload { pub enum DaemonFrame { StateUpdate(Box), StreamToken(String), - SystemNote { - kind: String, - message: String, - }, + SystemNote { kind: String, message: String }, ClipboardCopy(String), Closed, } diff --git a/apps/infrastructure/src/lib.rs b/apps/infrastructure/src/lib.rs index 4f85cbe..a36d1a6 100644 --- a/apps/infrastructure/src/lib.rs +++ b/apps/infrastructure/src/lib.rs @@ -67,8 +67,7 @@ use std::sync::Arc; pub use tools::{GraduatedCheck, Tool, ToolCtx, ToolCtxBuilder}; // Re-export commonly needed types at the crate root -pub use zesdex_domain::core::{ChatMessage, Role, Store, UsageStats, ToolCallResult}; - +pub use zesdex_domain::core::{ChatMessage, Role, Store, ToolCallResult, UsageStats}; /// A shared, async-writable cache of directory entries, used to avoid /// re-reading a directory every render frame. diff --git a/apps/infrastructure/src/llm/provider.rs b/apps/infrastructure/src/llm/provider.rs index d956648..22916a4 100644 --- a/apps/infrastructure/src/llm/provider.rs +++ b/apps/infrastructure/src/llm/provider.rs @@ -4,10 +4,10 @@ use rand_core::RngCore; use std::time::Duration; +use zesdex_application::ports::ProviderService; use zesdex_domain::core::{ ChatMessage, ChatRequest, ChatResponse, SseParser, StreamEvent, StreamOptions, ToolDef, }; -use zesdex_application::ports::ProviderService; use zesdex_domain::agent::defaults::{DEFAULT_API_BASE, DEFAULT_MODEL}; const DEFAULT_BASE_URL: &str = DEFAULT_API_BASE; @@ -82,10 +82,7 @@ impl LlmClient { retrying without connect timeout", e, ); - match reqwest::Client::builder() - .timeout(REQUEST_TIMEOUT) - .build() - { + match reqwest::Client::builder().timeout(REQUEST_TIMEOUT).build() { Ok(c) => c, Err(e2) => { tracing::warn!("also failed: {e2}. using default client"); @@ -115,8 +112,7 @@ impl LlmClient { .post(url) .header("Content-Type", "application/json"); if !self.api_key.is_empty() { - http_req = - http_req.header("Authorization", format!("Bearer {}", self.api_key)); + http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key)); } let mut resp = http_req.json(req).send().await.map_err(|e| { @@ -182,7 +178,7 @@ impl LlmClient { } let tc = &mut self.tool_calls[index]; - + if let Some(ref id_val) = id { tc.id = id_val.clone(); } @@ -237,8 +233,7 @@ impl LlmClient { n } }; - let text = - String::from_utf8_lossy(&byte_buf[..valid_len]).into_owned(); + let text = String::from_utf8_lossy(&byte_buf[..valid_len]).into_owned(); byte_buf.drain(..valid_len); for event in parser.feed(&text) { @@ -306,8 +301,7 @@ impl ProviderService for LlmClient { .header("Content-Type", "application/json"); if !self.api_key.is_empty() { - http_req = - http_req.header("Authorization", format!("Bearer {}", self.api_key)); + http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key)); } let result = async { @@ -335,9 +329,9 @@ impl ProviderService for LlmClient { } let data: ChatResponse = resp.json().await?; - let usage = data.usage.map(|u| { - (u64::from(u.prompt_tokens), u64::from(u.completion_tokens)) - }); + let usage = data + .usage + .map(|u| (u64::from(u.prompt_tokens), u64::from(u.completion_tokens))); let message = data .choices .into_iter() @@ -345,7 +339,8 @@ impl ProviderService for LlmClient { .and_then(|c| c.message) .ok_or_else(|| anyhow::anyhow!("API response had no choices"))?; Ok((message, usage)) - }.await; + } + .await; match result { Ok((msg, usage)) => return Ok((msg, usage)), @@ -393,14 +388,16 @@ impl ProviderService for LlmClient { let mut captured_content = false; let mut wrapped = |event: &StreamEvent| -> bool { match event { - StreamEvent::Token(_) | StreamEvent::Reasoning(_) | StreamEvent::ToolCallDelta { .. } => { + StreamEvent::Token(_) + | StreamEvent::Reasoning(_) + | StreamEvent::ToolCallDelta { .. } => { captured_content = true; } _ => {} } on_event(event) }; - + match self.try_stream_once(&req, &url, &mut wrapped).await { Ok(result) => return Ok(result), Err(e) => { @@ -425,11 +422,7 @@ pub fn resolve_api_key( ) -> String { let provider = &settings.provider; - let mut api_key = settings - .api_keys - .get(provider) - .cloned() - .unwrap_or_default(); + let mut api_key = settings.api_keys.get(provider).cloned().unwrap_or_default(); if api_key.is_empty() { if let Some(provider_cfg) = app_config.providers.get(provider) { diff --git a/apps/infrastructure/src/lsp/client.rs b/apps/infrastructure/src/lsp/client.rs index 7c92117..b27d602 100644 --- a/apps/infrastructure/src/lsp/client.rs +++ b/apps/infrastructure/src/lsp/client.rs @@ -32,8 +32,16 @@ impl LspClient { .stderr(Stdio::piped()) .spawn()?; - let stdin = child.stdin.take().ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?; - let stdout = BufReader::new(child.stdout.take().ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?); + let stdin = child + .stdin + .take() + .ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?; + let stdout = BufReader::new( + child + .stdout + .take() + .ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?, + ); info!("LSP client spawned: {command}"); Ok(LspClient { diff --git a/apps/infrastructure/src/lsp/manager.rs b/apps/infrastructure/src/lsp/manager.rs index 40a8d56..accd01f 100644 --- a/apps/infrastructure/src/lsp/manager.rs +++ b/apps/infrastructure/src/lsp/manager.rs @@ -28,7 +28,6 @@ impl Default for LspManager { } impl LspManager { - pub fn start(&mut self, language: &str, command: &str, args: &[String]) -> anyhow::Result<()> { let client = LspClient::start(command, args)?; self.clients.insert(language.to_string(), client); diff --git a/apps/infrastructure/src/lsp/provisioner/discovery.rs b/apps/infrastructure/src/lsp/provisioner/discovery.rs index ebe0d81..33dc888 100644 --- a/apps/infrastructure/src/lsp/provisioner/discovery.rs +++ b/apps/infrastructure/src/lsp/provisioner/discovery.rs @@ -9,8 +9,14 @@ fn known_configs() -> HashMap<&'static str, (&'static str, Vec<&'static str>)> { let mut m = HashMap::new(); m.insert("rust", ("rust-analyzer", vec![])); m.insert("python", ("pyright-langserver", vec!["--stdio"])); - m.insert("typescript", ("typescript-language-server", vec!["--stdio"])); - m.insert("javascript", ("typescript-language-server", vec!["--stdio"])); + m.insert( + "typescript", + ("typescript-language-server", vec!["--stdio"]), + ); + m.insert( + "javascript", + ("typescript-language-server", vec!["--stdio"]), + ); m.insert("go", ("gopls", vec![])); m } diff --git a/apps/infrastructure/src/lsp/provisioner/install.rs b/apps/infrastructure/src/lsp/provisioner/install.rs index ff1a653..69e7e7b 100644 --- a/apps/infrastructure/src/lsp/provisioner/install.rs +++ b/apps/infrastructure/src/lsp/provisioner/install.rs @@ -14,7 +14,10 @@ pub fn install_language_server(language: &str) -> anyhow::Result { if output.status.success() { Ok("rust-analyzer installed via rustup".to_string()) } else { - anyhow::bail!("failed to install rust-analyzer: {}", String::from_utf8_lossy(&output.stderr)) + anyhow::bail!( + "failed to install rust-analyzer: {}", + String::from_utf8_lossy(&output.stderr) + ) } } "python" => { @@ -24,7 +27,10 @@ pub fn install_language_server(language: &str) -> anyhow::Result { if output.status.success() { Ok("pyright installed via npm".to_string()) } else { - anyhow::bail!("failed to install pyright: {}", String::from_utf8_lossy(&output.stderr)) + anyhow::bail!( + "failed to install pyright: {}", + String::from_utf8_lossy(&output.stderr) + ) } } lang => anyhow::bail!("no install method known for language '{lang}'"), diff --git a/apps/infrastructure/src/lsp/provisioner/manager.rs b/apps/infrastructure/src/lsp/provisioner/manager.rs index 365561e..bb2386d 100644 --- a/apps/infrastructure/src/lsp/provisioner/manager.rs +++ b/apps/infrastructure/src/lsp/provisioner/manager.rs @@ -1,21 +1,21 @@ //! High-level manager that discovers, installs (if needed), and starts //! LSP servers. -use crate::lsp::manager::LspManager; use super::discovery::discover_installed; use super::install::install_language_server; +use crate::lsp::manager::LspManager; /// Auto-provision language servers for the given list of languages. /// /// Flow: discover already-installed servers → for each requested language /// not yet available, attempt auto-install → start each server. -pub fn auto_provision( - lsp_manager: &mut LspManager, - languages: &[String], -) -> Vec { +pub fn auto_provision(lsp_manager: &mut LspManager, languages: &[String]) -> Vec { let mut started = Vec::new(); let installed = discover_installed(); - let mut installed_map: std::collections::HashMap<&str, &crate::lsp::provisioner::config::LspProvisionerConfig> = std::collections::HashMap::new(); + let mut installed_map: std::collections::HashMap< + &str, + &crate::lsp::provisioner::config::LspProvisionerConfig, + > = std::collections::HashMap::new(); for cfg in &installed { installed_map.insert(cfg.language.as_str(), cfg); } diff --git a/apps/infrastructure/src/mcp/manager.rs b/apps/infrastructure/src/mcp/manager.rs index 6f98614..2e431c8 100644 --- a/apps/infrastructure/src/mcp/manager.rs +++ b/apps/infrastructure/src/mcp/manager.rs @@ -31,7 +31,6 @@ impl Default for McpManager { } impl McpManager { - /// Register an MCP server by name and transport string. /// /// Returns an error if a server with the same name is already registered. diff --git a/apps/infrastructure/src/middleware/auth.rs b/apps/infrastructure/src/middleware/auth.rs index 5c6f9a4..91ec65b 100644 --- a/apps/infrastructure/src/middleware/auth.rs +++ b/apps/infrastructure/src/middleware/auth.rs @@ -114,7 +114,11 @@ where } Box::pin(async move { - Ok((StatusCode::UNAUTHORIZED, "missing or invalid X-Session-Id header").into_response()) + Ok(( + StatusCode::UNAUTHORIZED, + "missing or invalid X-Session-Id header", + ) + .into_response()) }) } } diff --git a/apps/infrastructure/src/middleware/cors.rs b/apps/infrastructure/src/middleware/cors.rs deleted file mode 100644 index 3134c60..0000000 --- a/apps/infrastructure/src/middleware/cors.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! CORS layer factory for the daemon HTTP server. - -use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer}; - -/// Return a permissive CorsLayer for local daemon IPC. -/// -/// All method and header names are static strings guaranteed to be valid -/// HTTP tokens — `.parse()` is infallible here. -pub fn default_cors_layer() -> CorsLayer { - CorsLayer::new() - .allow_origin(AllowOrigin::any()) - .allow_methods([ - "GET".parse().expect("static HTTP method"), - "POST".parse().expect("static HTTP method"), - "PUT".parse().expect("static HTTP method"), - "DELETE".parse().expect("static HTTP method"), - "PATCH".parse().expect("static HTTP method"), - "OPTIONS".parse().expect("static HTTP method"), - ]) - .allow_headers(AllowHeaders::any()) - .expose_headers([ - "Content-Type".parse().expect("static HTTP header"), - "X-Session-Id".parse().expect("static HTTP header"), - "X-Request-Id".parse().expect("static HTTP header"), - ]) -} diff --git a/apps/infrastructure/src/middleware/mod.rs b/apps/infrastructure/src/middleware/mod.rs index dea8ef0..19eb161 100644 --- a/apps/infrastructure/src/middleware/mod.rs +++ b/apps/infrastructure/src/middleware/mod.rs @@ -1,5 +1,4 @@ //! Axum middleware tower for the HTTP API layer. pub mod auth; -pub mod cors; pub mod rate_limit; diff --git a/apps/infrastructure/src/middleware/rate_limit.rs b/apps/infrastructure/src/middleware/rate_limit.rs index 66dedca..1dcca92 100644 --- a/apps/infrastructure/src/middleware/rate_limit.rs +++ b/apps/infrastructure/src/middleware/rate_limit.rs @@ -28,11 +28,14 @@ impl RateLimiter { .as_secs() as i64; let cutoff = now.saturating_sub(window_secs as i64); - let mut windows = self.windows.lock().map_err(|e| { - anyhow::anyhow!("rate limiter lock poisoned: {e}") - })?; + let mut windows = self + .windows + .lock() + .map_err(|e| anyhow::anyhow!("rate limiter lock poisoned: {e}"))?; - let timestamps = windows.entry(client_id.to_string()).or_insert_with(Vec::new); + let timestamps = windows + .entry(client_id.to_string()) + .or_insert_with(Vec::new); timestamps.retain(|&ts| ts >= cutoff); if timestamps.len() >= max_requests as usize { diff --git a/apps/infrastructure/src/persistence/cms/app_config_repo.rs b/apps/infrastructure/src/persistence/cms/app_config_repo.rs index 2175503..aebd484 100644 --- a/apps/infrastructure/src/persistence/cms/app_config_repo.rs +++ b/apps/infrastructure/src/persistence/cms/app_config_repo.rs @@ -3,7 +3,9 @@ use std::path::Path; use serde::{Deserialize, Serialize}; -use zesdex_domain::cms::{AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError}; +use zesdex_domain::cms::{ + AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError, +}; use crate::utils::write_json_atomic; @@ -40,12 +42,15 @@ fn claude_settings_from_file() -> Option { fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option)> { let settings = claude_settings_from_file(); - + let file_creds = settings.as_ref().and_then(|s| { let env = s.env.as_ref()?; - Some((env.anthropic_base_url.clone()?, env.anthropic_api_key.clone()?)) + Some(( + env.anthropic_base_url.clone()?, + env.anthropic_api_key.clone()?, + )) }); - + let env_creds = || -> Option<(String, String)> { let base_url = std::env::var("ANTHROPIC_BASE_URL").ok()?; let key = std::env::var("ANTHROPIC_API_KEY").ok()?; @@ -55,7 +60,7 @@ fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option)> let custom_model = settings.and_then(|s| s.custom_model); let (base_url, key) = file_creds.or_else(env_creds)?; - + Some(( ProviderConfig { api_base: base_url, @@ -63,7 +68,7 @@ fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option)> default_model: custom_model.clone(), default_api_key: Some(key), }, - custom_model + custom_model, )) } @@ -72,9 +77,7 @@ impl AppConfigRepository for JsonAppConfigRepository { let path = base_dir.join("app_config.json"); let mut cfg: AppConfig = match std::fs::read_to_string(&path) { Ok(s) => serde_json::from_str(&s)?, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - AppConfig::default() - } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => AppConfig::default(), Err(e) => return Err(RepositoryError::Io(e)), }; @@ -106,15 +109,13 @@ impl AppConfigRepository for JsonAppConfigRepository { } if let Some(custom) = &custom_model { - cfg.model_roles - .entry(custom.clone()) - .or_insert(ModelRole { - provider: "claude".to_string(), - model: custom.clone(), - max_tokens: Some(8192), - context_window: Some(200_000), - temperature: Some(0.7), - }); + cfg.model_roles.entry(custom.clone()).or_insert(ModelRole { + provider: "claude".to_string(), + model: custom.clone(), + max_tokens: Some(8192), + context_window: Some(200_000), + temperature: Some(0.7), + }); } if cfg.default_provider == defaults.default_provider { diff --git a/apps/infrastructure/src/persistence/cms/memory_repo.rs b/apps/infrastructure/src/persistence/cms/memory_repo.rs index d35f0cd..994ee04 100644 --- a/apps/infrastructure/src/persistence/cms/memory_repo.rs +++ b/apps/infrastructure/src/persistence/cms/memory_repo.rs @@ -77,10 +77,7 @@ impl MarkdownMemoryRepository { .lines() .filter_map(|l| { let mut it = l.splitn(2, ':'); - Some(( - it.next()?.trim().to_string(), - it.next()?.trim().to_string(), - )) + Some((it.next()?.trim().to_string(), it.next()?.trim().to_string())) }) .collect() } @@ -175,7 +172,9 @@ impl MemoryRepository for MarkdownMemoryRepository { fn save(&self, memory_dir: &Path, memory: &Memory) -> Result<(), RepositoryError> { let path = Memory::path(memory_dir, &memory.name); - let parent = path.parent().expect("memory path always has a parent directory"); + let parent = path + .parent() + .expect("memory path always has a parent directory"); std::fs::create_dir_all(parent)?; let frontmatter = Self::build_frontmatter(memory); diff --git a/apps/infrastructure/src/persistence/cms/settings_repo.rs b/apps/infrastructure/src/persistence/cms/settings_repo.rs index a8cb984..4a00595 100644 --- a/apps/infrastructure/src/persistence/cms/settings_repo.rs +++ b/apps/infrastructure/src/persistence/cms/settings_repo.rs @@ -21,16 +21,16 @@ impl SettingsRepository for JsonSettingsRepository { fn load(&self, base_dir: &Path) -> Result { let path = base_dir.join("settings.json"); match std::fs::read_to_string(&path) { - Ok(s) => match serde_json::from_str(&s) { - Ok(settings) => Ok(settings), - Err(e) => { - tracing::warn!("settings.json at '{:?}' failed to parse ({e}); falling back to defaults", path); - Ok(Settings::default()) + Ok(s) => { + match serde_json::from_str(&s) { + Ok(settings) => Ok(settings), + Err(e) => { + tracing::warn!("settings.json at '{:?}' failed to parse ({e}); falling back to defaults", path); + Ok(Settings::default()) + } } - }, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - Ok(Settings::default()) } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Settings::default()), Err(e) => Err(RepositoryError::Io(e)), } } diff --git a/apps/infrastructure/src/persistence/iam/session_lock_repo.rs b/apps/infrastructure/src/persistence/iam/session_lock_repo.rs index 739d73e..780ab1f 100644 --- a/apps/infrastructure/src/persistence/iam/session_lock_repo.rs +++ b/apps/infrastructure/src/persistence/iam/session_lock_repo.rs @@ -50,9 +50,7 @@ impl SessionLockRepository for FileSystemSessionLockRepository { .write(true) .open(&tmp) .map_err(|_| { - RepositoryError::Other( - "another process is replacing the lock".to_string(), - ) + RepositoryError::Other("another process is replacing the lock".to_string()) })?; write!(tmp_file, "{pid}")?; tmp_file.sync_all()?; diff --git a/apps/infrastructure/src/persistence/mod.rs b/apps/infrastructure/src/persistence/mod.rs index ddc36a5..2ade260 100644 --- a/apps/infrastructure/src/persistence/mod.rs +++ b/apps/infrastructure/src/persistence/mod.rs @@ -5,16 +5,12 @@ pub mod cms; pub mod iam; pub mod sqlite; +pub use cms::{ + app_config_repo::JsonAppConfigRepository, conversation_repo::JsonConversationRepository, + edit_log_repo::JsonlEditLogRepository, memory_repo::MarkdownMemoryRepository, + rewind_blob_repo::FileRewindBlobRepository, settings_repo::JsonSettingsRepository, +}; pub use iam::{ - oauth_repo::FileSystemOAuthRepository, - session_lock_repo::FileSystemSessionLockRepository, + oauth_repo::FileSystemOAuthRepository, session_lock_repo::FileSystemSessionLockRepository, session_repo::FileSystemSessionRepository, }; -pub use cms::{ - app_config_repo::JsonAppConfigRepository, - conversation_repo::JsonConversationRepository, - edit_log_repo::JsonlEditLogRepository, - memory_repo::MarkdownMemoryRepository, - rewind_blob_repo::FileRewindBlobRepository, - settings_repo::JsonSettingsRepository, -}; diff --git a/apps/infrastructure/src/subagent/auto/engine.rs b/apps/infrastructure/src/subagent/auto/engine.rs index d069957..4cc5f37 100644 --- a/apps/infrastructure/src/subagent/auto/engine.rs +++ b/apps/infrastructure/src/subagent/auto/engine.rs @@ -160,7 +160,8 @@ pub fn spawn_background_review( SEVERITY: HIGH|MEDIUM|LOW\n\ OLD: \n\ NEW: \n\ - ---".to_string(), + ---" + .to_string(), ); let user_msg = ChatMessage::user(format!( @@ -210,7 +211,11 @@ pub fn spawn_background_review( &turn_events, TurnEvent::SystemNote { kind: "review_finding".into(), - message: format!("📋 Auto-review complete ({} fix(es) applied).\n{}", fix_count, response_text.trim()), + message: format!( + "📋 Auto-review complete ({} fix(es) applied).\n{}", + fix_count, + response_text.trim() + ), }, ); info!(fix_count, "auto-review: completed with fixes"); @@ -237,11 +242,7 @@ async fn run_llm_review(client: &LlmClient, messages: &[ChatMessage]) -> Result< } /// Parse the LLM response for structured fix commands and apply them. -fn apply_fixes_from_response( - response: &str, - tools: &[Box], - tool_ctx: &ToolCtx, -) -> usize { +fn apply_fixes_from_response(response: &str, tools: &[Box], tool_ctx: &ToolCtx) -> usize { let mut fix_count = 0; // Parse structured fix blocks diff --git a/apps/infrastructure/src/subagent/engine.rs b/apps/infrastructure/src/subagent/engine.rs index 92d2bf7..3eb2f0b 100644 --- a/apps/infrastructure/src/subagent/engine.rs +++ b/apps/infrastructure/src/subagent/engine.rs @@ -98,10 +98,7 @@ pub async fn run_agent( // If no tool calls, we're done — return content if tool_calls.is_empty() { info!("Subagent completed after {iteration} iterations"); - report_progress( - &tool_ctx, - AgentProgress::completed("subagent", directive), - ); + report_progress(&tool_ctx, AgentProgress::completed("subagent", directive)); return Ok(content); } @@ -121,15 +118,14 @@ pub async fn run_agent( ), ); - let result = - if let Some(tool) = tools.iter().find(|t| t.name() == tool_name) { - match tool.run(&tool_ctx, &args) { - Ok(output) => output, - Err(e) => format!("Error: {e}"), - } - } else { - format!("Unknown tool: {tool_name}") - }; + let result = if let Some(tool) = tools.iter().find(|t| t.name() == tool_name) { + match tool.run(&tool_ctx, &args) { + Ok(output) => output, + Err(e) => format!("Error: {e}"), + } + } else { + format!("Unknown tool: {tool_name}") + }; messages.push(ChatMessage::tool(tc.id.clone(), result)); } @@ -149,5 +145,7 @@ pub async fn run_agent( format!("iteration limit ({MAX_ITERATIONS})"), ), ); - Ok(format!("Subagent reached iteration limit ({MAX_ITERATIONS})")) + Ok(format!( + "Subagent reached iteration limit ({MAX_ITERATIONS})" + )) } diff --git a/apps/infrastructure/src/subagent/provider.rs b/apps/infrastructure/src/subagent/provider.rs index 786123f..5c7eefe 100644 --- a/apps/infrastructure/src/subagent/provider.rs +++ b/apps/infrastructure/src/subagent/provider.rs @@ -40,9 +40,7 @@ impl SubagentProvider { messages: &[ChatMessage], ) -> Result<(ChatMessage, Option<(u64, u64)>)> { use zesdex_application::ports::ProviderService; - self.client - .chat(messages, None, Some(4096), None) - .await + self.client.chat(messages, None, Some(4096), None).await } /// Send messages with available tool definitions. diff --git a/apps/infrastructure/src/tools/best_practice.rs b/apps/infrastructure/src/tools/best_practice.rs index c187fb4..234adaa 100644 --- a/apps/infrastructure/src/tools/best_practice.rs +++ b/apps/infrastructure/src/tools/best_practice.rs @@ -131,7 +131,8 @@ impl Tool for BestPractice { } // Suggest a template. if let Some(parsed) = eng.parse_commit(&msg) { - let tpl = eng.suggest_commit_template(&parsed.type_, parsed.scope.as_deref()); + let tpl = + eng.suggest_commit_template(&parsed.type_, parsed.scope.as_deref()); out.push_str(&format!("\nTemplate: {tpl}\n")); } Ok(out) @@ -309,8 +310,14 @@ mod tests { let tool = BestPractice; let args = json!({"action": "list_skills"}); let result = tool.run(&test_ctx(), &args).unwrap(); - assert!(result.contains("clean-code"), "should list clean-code: {result}"); - assert!(result.contains("commit-convention"), "should list commit-convention: {result}"); + assert!( + result.contains("clean-code"), + "should list clean-code: {result}" + ); + assert!( + result.contains("commit-convention"), + "should list commit-convention: {result}" + ); } #[test] @@ -318,7 +325,10 @@ mod tests { let tool = BestPractice; let args = json!({"action": "get_skill", "skill_name": "clean-code"}); let result = tool.run(&test_ctx(), &args).unwrap(); - assert!(result.contains("Clean Code"), "should contain skill content: {result}"); + assert!( + result.contains("Clean Code"), + "should contain skill content: {result}" + ); } #[test] @@ -326,7 +336,10 @@ mod tests { let tool = CommitConvention; let args = json!({"message": "feat(tool): add best practice audit"}); let result = tool.run(&test_ctx(), &args).unwrap(); - assert!(result.contains("✅"), "valid commit should succeed: {result}"); + assert!( + result.contains("✅"), + "valid commit should succeed: {result}" + ); } #[test] @@ -334,6 +347,9 @@ mod tests { let tool = CommitConvention; let args = json!({"message": "Add new feature"}); let result = tool.run(&test_ctx(), &args).unwrap(); - assert!(result.contains("❌"), "invalid commit should fail: {result}"); + assert!( + result.contains("❌"), + "invalid commit should fail: {result}" + ); } } diff --git a/apps/infrastructure/src/tools/context.rs b/apps/infrastructure/src/tools/context.rs index 4ba2fcd..19bde5e 100644 --- a/apps/infrastructure/src/tools/context.rs +++ b/apps/infrastructure/src/tools/context.rs @@ -17,8 +17,7 @@ pub struct ToolCtx { pub origin: crate::Origin, pub graduated_checks: Vec, pub lsp_manager: Arc>, - pub turn_events: - Option>>>, + pub turn_events: Option>>>, pub workflow_findings: Option>>>, pub abort_flag: Option>, } @@ -41,8 +40,7 @@ pub struct ToolCtxBuilder { pub origin: crate::Origin, pub graduated_checks: Vec, pub lsp_manager: Arc>, - pub turn_events: - Option>>>, + pub turn_events: Option>>>, pub workflow_findings: Option>>>, pub abort_flag: Option>, } diff --git a/apps/infrastructure/src/tools/executor.rs b/apps/infrastructure/src/tools/executor.rs index 3681bb8..4580e8f 100644 --- a/apps/infrastructure/src/tools/executor.rs +++ b/apps/infrastructure/src/tools/executor.rs @@ -1,7 +1,7 @@ -use std::future::Future; -use anyhow::Result; -use zesdex_application::agent::ToolExecutor; use crate::tools::{all_tools, Tool, ToolCtx}; +use anyhow::Result; +use std::future::Future; +use zesdex_application::agent::ToolExecutor; pub struct InfrastructureToolExecutor { ctx: ToolCtx, @@ -27,14 +27,10 @@ impl ToolExecutor for InfrastructureToolExecutor { let tool_opt = self.tools.iter().find(|t| t.name() == tool_name); let ctx = self.ctx.clone(); let args = args.clone(); - + async move { match tool_opt { - Some(tool) => { - tokio::task::block_in_place(move || { - tool.run(&ctx, &args) - }) - } + Some(tool) => tokio::task::block_in_place(move || tool.run(&ctx, &args)), None => { anyhow::bail!("Unknown tool: {}", tool_name) } diff --git a/apps/infrastructure/src/tools/git/git_operator.rs b/apps/infrastructure/src/tools/git/git_operator.rs index e5ada59..52aef96 100644 --- a/apps/infrastructure/src/tools/git/git_operator.rs +++ b/apps/infrastructure/src/tools/git/git_operator.rs @@ -7,7 +7,7 @@ use crate::tools::shell_filter::git::check_git_destructive; use crate::tools::{execute_cmd, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; -use tracing::{info, warn, instrument}; +use tracing::{info, instrument, warn}; /// Tool that executes safe git operations (status, log, diff, commit, branch, etc.). /// diff --git a/apps/infrastructure/src/tools/git/git_worktree.rs b/apps/infrastructure/src/tools/git/git_worktree.rs index ff25b03..e38dd3a 100644 --- a/apps/infrastructure/src/tools/git/git_worktree.rs +++ b/apps/infrastructure/src/tools/git/git_worktree.rs @@ -56,15 +56,13 @@ impl Tool for GitWorktree { let branch = crate::tools::arg_str(args, "branch")?; info!(path, branch, "adding worktree"); let output = execute_cmd( - std::process::Command::new("git") - .args(["worktree", "add", &path, &branch]), + std::process::Command::new("git").args(["worktree", "add", &path, &branch]), )?; Ok(output) } "list" => { - let output = execute_cmd( - std::process::Command::new("git").args(["worktree", "list"]), - )?; + let output = + execute_cmd(std::process::Command::new("git").args(["worktree", "list"]))?; Ok(output) } "remove" => { @@ -77,9 +75,8 @@ impl Tool for GitWorktree { } "prune" => { info!("pruning stale worktree metadata"); - let output = execute_cmd( - std::process::Command::new("git").args(["worktree", "prune"]), - )?; + let output = + execute_cmd(std::process::Command::new("git").args(["worktree", "prune"]))?; Ok(output) } _ => anyhow::bail!("unknown action: {}", action), diff --git a/apps/infrastructure/src/tools/graduated.rs b/apps/infrastructure/src/tools/graduated.rs index 97bc439..852978b 100644 --- a/apps/infrastructure/src/tools/graduated.rs +++ b/apps/infrastructure/src/tools/graduated.rs @@ -11,11 +11,7 @@ pub struct GraduatedCheck { } /// Check which graduated checks apply to a given file path/content pair. -pub fn check_graduated_checks( - path: &str, - content: &str, - checks: &[GraduatedCheck], -) -> Vec { +pub fn check_graduated_checks(path: &str, content: &str, checks: &[GraduatedCheck]) -> Vec { let mut matches = Vec::new(); for check in checks { if path.contains(&check.pattern) || content.contains(&check.rule) { diff --git a/apps/infrastructure/src/tools/lsp/completion.rs b/apps/infrastructure/src/tools/lsp/completion.rs index 07afade..2ab6c47 100644 --- a/apps/infrastructure/src/tools/lsp/completion.rs +++ b/apps/infrastructure/src/tools/lsp/completion.rs @@ -6,7 +6,7 @@ use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; -use tracing::{info, error, instrument}; +use tracing::{error, info, instrument}; /// Tool that requests code completion suggestions from an LSP server. /// @@ -65,10 +65,13 @@ impl Tool for LspCompletion { } }; if let Some(client) = manager.get_client(&language) { - let result = client.send_request("textDocument/completion", &json!({ - "textDocument": { "uri": format!("file://{}", path) }, - "position": { "line": line, "character": character } - }))?; + let result = client.send_request( + "textDocument/completion", + &json!({ + "textDocument": { "uri": format!("file://{}", path) }, + "position": { "line": line, "character": character } + }), + )?; Ok(serde_json::to_string_pretty(&result)?) } else { anyhow::bail!("no LSP client connected for '{language}'") diff --git a/apps/infrastructure/src/tools/lsp/connect.rs b/apps/infrastructure/src/tools/lsp/connect.rs index e58a570..b922731 100644 --- a/apps/infrastructure/src/tools/lsp/connect.rs +++ b/apps/infrastructure/src/tools/lsp/connect.rs @@ -6,7 +6,7 @@ use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; -use tracing::{info, error, instrument}; +use tracing::{error, info, instrument}; /// Tool that connects to an LSP language server for a given language. /// @@ -52,7 +52,11 @@ impl Tool for LspConnect { let extra_args: Vec = args .get("args") .and_then(|v| v.as_array()) - .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect() + }) .unwrap_or_default(); info!(language, command, extra_args = ?extra_args, "LSP connect requested"); diff --git a/apps/infrastructure/src/tools/lsp/definition.rs b/apps/infrastructure/src/tools/lsp/definition.rs index e5e216c..3d5436f 100644 --- a/apps/infrastructure/src/tools/lsp/definition.rs +++ b/apps/infrastructure/src/tools/lsp/definition.rs @@ -6,7 +6,7 @@ use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; -use tracing::{info, error, instrument}; +use tracing::{error, info, instrument}; /// Tool that resolves a symbol's definition location via LSP. /// @@ -66,10 +66,13 @@ impl Tool for LspDefinition { } }; if let Some(client) = manager.get_client(&language) { - let result = client.send_request("textDocument/definition", &json!({ - "textDocument": { "uri": format!("file://{}", path) }, - "position": { "line": line, "character": character } - }))?; + let result = client.send_request( + "textDocument/definition", + &json!({ + "textDocument": { "uri": format!("file://{}", path) }, + "position": { "line": line, "character": character } + }), + )?; Ok(serde_json::to_string_pretty(&result)?) } else { anyhow::bail!("no LSP client connected for '{language}'") diff --git a/apps/infrastructure/src/tools/lsp/diagnostics.rs b/apps/infrastructure/src/tools/lsp/diagnostics.rs index fe003d3..342c0e2 100644 --- a/apps/infrastructure/src/tools/lsp/diagnostics.rs +++ b/apps/infrastructure/src/tools/lsp/diagnostics.rs @@ -7,7 +7,7 @@ use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; -use tracing::{info, error, instrument}; +use tracing::{error, info, instrument}; /// Tool that retrieves diagnostics (errors, warnings) from the LSP for a file. /// @@ -56,9 +56,12 @@ impl Tool for LspDiagnostics { } }; if let Some(client) = manager.get_client(&language) { - let result = client.send_request("textDocument/diagnostic", &json!({ - "textDocument": { "uri": format!("file://{}", path) } - }))?; + let result = client.send_request( + "textDocument/diagnostic", + &json!({ + "textDocument": { "uri": format!("file://{}", path) } + }), + )?; Ok(serde_json::to_string_pretty(&result)?) } else { anyhow::bail!("no LSP client connected for '{language}'") diff --git a/apps/infrastructure/src/tools/lsp/disconnect.rs b/apps/infrastructure/src/tools/lsp/disconnect.rs index 7384dfc..e303894 100644 --- a/apps/infrastructure/src/tools/lsp/disconnect.rs +++ b/apps/infrastructure/src/tools/lsp/disconnect.rs @@ -6,7 +6,7 @@ use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; -use tracing::{info, error, instrument}; +use tracing::{error, info, instrument}; /// Tool that disconnects an LSP language server for a given language. /// diff --git a/apps/infrastructure/src/tools/lsp/hover.rs b/apps/infrastructure/src/tools/lsp/hover.rs index 58221ae..9831c68 100644 --- a/apps/infrastructure/src/tools/lsp/hover.rs +++ b/apps/infrastructure/src/tools/lsp/hover.rs @@ -6,7 +6,7 @@ use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; -use tracing::{info, error, instrument}; +use tracing::{error, info, instrument}; /// Tool that retrieves hover information for a symbol at a position via LSP. /// @@ -66,10 +66,13 @@ impl Tool for LspHover { } }; if let Some(client) = manager.get_client(&language) { - let result = client.send_request("textDocument/hover", &json!({ - "textDocument": { "uri": format!("file://{}", path) }, - "position": { "line": line, "character": character } - }))?; + let result = client.send_request( + "textDocument/hover", + &json!({ + "textDocument": { "uri": format!("file://{}", path) }, + "position": { "line": line, "character": character } + }), + )?; Ok(serde_json::to_string_pretty(&result)?) } else { anyhow::bail!("no LSP client connected for '{language}'") diff --git a/apps/infrastructure/src/tools/lsp/mod.rs b/apps/infrastructure/src/tools/lsp/mod.rs index 33348f4..e47a760 100644 --- a/apps/infrastructure/src/tools/lsp/mod.rs +++ b/apps/infrastructure/src/tools/lsp/mod.rs @@ -9,10 +9,10 @@ pub mod disconnect; pub mod hover; pub mod references; -pub use connect::LspConnect; -pub use diagnostics::LspDiagnostics; -pub use hover::LspHover; pub use completion::LspCompletion; +pub use connect::LspConnect; pub use definition::LspDefinition; -pub use references::LspReferences; +pub use diagnostics::LspDiagnostics; pub use disconnect::LspDisconnect; +pub use hover::LspHover; +pub use references::LspReferences; diff --git a/apps/infrastructure/src/tools/lsp/references.rs b/apps/infrastructure/src/tools/lsp/references.rs index 8215691..7127543 100644 --- a/apps/infrastructure/src/tools/lsp/references.rs +++ b/apps/infrastructure/src/tools/lsp/references.rs @@ -6,7 +6,7 @@ use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; -use tracing::{info, error, instrument}; +use tracing::{error, info, instrument}; /// Tool that finds all references to a symbol at a position via LSP. /// @@ -66,10 +66,13 @@ impl Tool for LspReferences { } }; if let Some(client) = manager.get_client(&language) { - let result = client.send_request("textDocument/references", &json!({ - "textDocument": { "uri": format!("file://{}", path) }, - "position": { "line": line, "character": character } - }))?; + let result = client.send_request( + "textDocument/references", + &json!({ + "textDocument": { "uri": format!("file://{}", path) }, + "position": { "line": line, "character": character } + }), + )?; Ok(serde_json::to_string_pretty(&result)?) } else { anyhow::bail!("no LSP client connected for '{language}'") diff --git a/apps/infrastructure/src/tools/mod.rs b/apps/infrastructure/src/tools/mod.rs index cc1402e..e1daa2a 100644 --- a/apps/infrastructure/src/tools/mod.rs +++ b/apps/infrastructure/src/tools/mod.rs @@ -52,8 +52,8 @@ pub mod sequential_think; pub mod shell; pub mod shell_filter; pub mod spawn; -pub mod utility; pub mod util; +pub mod utility; pub mod web_search; pub mod workflow; diff --git a/apps/infrastructure/src/tools/parallel_delegate.rs b/apps/infrastructure/src/tools/parallel_delegate.rs index 2772865..461000e 100644 --- a/apps/infrastructure/src/tools/parallel_delegate.rs +++ b/apps/infrastructure/src/tools/parallel_delegate.rs @@ -112,10 +112,7 @@ impl Tool for ParallelDelegate { dirs.iter() .filter_map(|d| { let directive = d.get("directive").and_then(|v| v.as_str())?; - let access_str = d - .get("access") - .and_then(|v| v.as_str()) - .unwrap_or("write"); + let access_str = d.get("access").and_then(|v| v.as_str()).unwrap_or("write"); let access = match access_str { "read" => AccessTier::Read, "full" => AccessTier::Full, @@ -162,12 +159,7 @@ impl Tool for ParallelDelegate { ); debug!(agent_index = i, access = ?access, "spawning parallel agent"); - let handle = spawn_subagent( - subagent_ctx, - directive.clone(), - *access, - ctx.clone(), - ); + let handle = spawn_subagent(subagent_ctx, directive.clone(), *access, ctx.clone()); handles.push((i, handle)); } @@ -181,11 +173,7 @@ impl Tool for ParallelDelegate { } Ok(Err(e)) => { warn!(agent_index = i, error = %e, "parallel agent failed"); - results.push(( - i, - directives[i].0.clone(), - format!("[ERROR] {e}"), - )); + results.push((i, directives[i].0.clone(), format!("[ERROR] {e}"))); } Err(e) => { warn!(agent_index = i, error = ?e, "parallel agent panicked"); @@ -201,7 +189,8 @@ impl Tool for ParallelDelegate { // Consolidate results if synthesize && results.len() > 1 { let rt = tokio::runtime::Runtime::new()?; - let consolidated = rt.block_on(consolidate_results(&results, &base_url, &api_key, &model))?; + let consolidated = + rt.block_on(consolidate_results(&results, &base_url, &api_key, &model))?; Ok(format!( "## Parallel Delegation Complete\n\n**Task:** {task}\n**Parallel agents:** {}\n\n{}", results.len(), @@ -213,7 +202,10 @@ impl Tool for ParallelDelegate { results.len() ); for (i, directive, result) in &results { - output.push_str(&format!("---\n### Agent {}: {}\n\n{}\n", i, directive, result)); + output.push_str(&format!( + "---\n### Agent {}: {}\n\n{}\n", + i, directive, result + )); } Ok(output) } @@ -234,9 +226,8 @@ async fn auto_split_task( Some(base_url.to_string()), ); - let sys_msg = zesdex_domain::core::ChatMessage::system( - format!( - "You are a task decomposition expert. Split the following task into {max_parallel} \ + let sys_msg = zesdex_domain::core::ChatMessage::system(format!( + "You are a task decomposition expert. Split the following task into {max_parallel} \ independent sub-tasks that can run in parallel. Each sub-task must be self-contained \ and produce useful output independently.\n\n\ Output your response as a JSON array of objects, each with:\n\ @@ -244,15 +235,16 @@ async fn auto_split_task( - \"access\": one of \"read\", \"write\", or \"full\"\n\n\ IMPORTANT: Return ONLY valid JSON, no other text. Example:\n\ [{{\"directive\": \"Create the User model with fields...\", \"access\": \"write\"}}]" - ) - ); + )); - let user_msg = - zesdex_domain::core::ChatMessage::user(format!("Task: {task}\n\nSplit into {max_parallel} parallel directives:")); + let user_msg = zesdex_domain::core::ChatMessage::user(format!( + "Task: {task}\n\nSplit into {max_parallel} parallel directives:" + )); use zesdex_application::ports::ProviderService; match client - .chat(&[sys_msg, user_msg], None, Some(2048), Some(0.4)).await + .chat(&[sys_msg, user_msg], None, Some(2048), Some(0.4)) + .await { Ok((response, _)) => { let text = response.content.unwrap_or_default(); @@ -320,10 +312,7 @@ fn fallback_split(task: &str, max_parallel: usize) -> Vec<(String, AccessTier)> } if task.contains("test") || task.contains("unit") { - directives.push(( - format!("Write unit tests for: {task}"), - AccessTier::Read, - )); + directives.push((format!("Write unit tests for: {task}"), AccessTier::Read)); } if directives.is_empty() { @@ -370,7 +359,10 @@ async fn consolidate_results( )); use zesdex_application::ports::ProviderService; - match client.chat(&[sys_msg, user_msg], None, Some(2048), Some(0.3)).await { + match client + .chat(&[sys_msg, user_msg], None, Some(2048), Some(0.3)) + .await + { Ok((response, _)) => Ok(response.content.unwrap_or_else(|| summary.clone())), Err(e) => { warn!(error = %e, "consolidation LLM call failed, using raw concatenation"); diff --git a/apps/infrastructure/src/tools/search.rs b/apps/infrastructure/src/tools/search.rs index e237a73..297543d 100644 --- a/apps/infrastructure/src/tools/search.rs +++ b/apps/infrastructure/src/tools/search.rs @@ -158,11 +158,7 @@ impl Tool for Glob { let p = entry.path(); if glob_set.is_match(p) { let rel_path = p.strip_prefix(&root).unwrap_or(p).display().to_string(); - matches.push(format!( - "{}{}", - rel_path, - if p.is_dir() { "/" } else { "" } - )); + matches.push(format!("{}{}", rel_path, if p.is_dir() { "/" } else { "" })); } } matches.sort(); diff --git a/apps/infrastructure/src/tools/semantic_search.rs b/apps/infrastructure/src/tools/semantic_search.rs index 5f19d64..55c1e2b 100644 --- a/apps/infrastructure/src/tools/semantic_search.rs +++ b/apps/infrastructure/src/tools/semantic_search.rs @@ -304,10 +304,7 @@ impl SymbolIndex { continue; } - let ext = file_path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or(""); + let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or(""); let extractor = match ext_dispatch.get(ext) { Some(f) => f, _ => continue, // unsupported extension @@ -704,8 +701,8 @@ fn extract_typescript(content: &str, rel_path: &str) -> Vec { if let Some(caps) = r.ts_var_export.captures(trimmed) { let name = caps.get(1).unwrap().as_str().to_string(); // Only capture top-level (indentation 0) or exported - let is_top_level = line.starts_with(|c: char| !c.is_whitespace()) - || trimmed.starts_with("export"); + let is_top_level = + line.starts_with(|c: char| !c.is_whitespace()) || trimmed.starts_with("export"); if is_top_level { let kind = if trimmed.contains("const ") { SymbolKind::Constant @@ -963,10 +960,7 @@ fn extract_python(content: &str, rel_path: &str) -> Vec { && !trimmed.contains("=="); if is_top_level { let name = trimmed.split('=').next().unwrap_or("").trim().to_string(); - if !name.is_empty() - && !name.starts_with('_') - && !name.contains(' ') - { + if !name.is_empty() && !name.starts_with('_') && !name.contains(' ') { let kind = if name.chars().all(|c| c.is_uppercase() || c == '_') { SymbolKind::Constant } else { @@ -1157,10 +1151,7 @@ pub fn format_symbol_listing(index: &SymbolIndex) -> String { std::collections::BTreeMap::new(); for sym in syms { let kind_str = sym.kind.to_string(); - by_kind - .entry(kind_str) - .or_default() - .push(sym.name.as_str()); + by_kind.entry(kind_str).or_default().push(sym.name.as_str()); } for (kind, names) in &by_kind { out.push_str(&format!(" {kind}: {}\n", names.join(", "))); @@ -1312,7 +1303,11 @@ impl Tool for SemanticSearch { } let total = index.len(); - info!(matched = filtered.len(), total_indexed = total, "semantic search completed"); + info!( + matched = filtered.len(), + total_indexed = total, + "semantic search completed" + ); let mut by_file: std::collections::BTreeMap> = std::collections::BTreeMap::new(); @@ -1350,7 +1345,11 @@ impl Tool for SemanticSearch { sym.line, sym.context.trim(), doc_str, - if sym.context.trim().len() > 80 { "…" } else { "" } + if sym.context.trim().len() > 80 { + "…" + } else { + "" + } )); } output.push('\n'); @@ -1574,12 +1573,7 @@ impl Tool for ListSymbols { for (file, file_syms) in &by_file { out.push_str(&format!("`{file}`:\n")); for sym in file_syms { - out.push_str(&format!( - " `{}` {} L{}\n", - sym.kind, - sym.name, - sym.line, - )); + out.push_str(&format!(" `{}` {} L{}\n", sym.kind, sym.name, sym.line,)); } } out.push('\n'); @@ -1633,51 +1627,82 @@ mod tests { fn test_extract_typescript_function_and_class() { let content = "function hello() {}\nexport class User {}\ninterface Person {}\n"; let symbols = extract_typescript(content, "test.ts"); - assert!(symbols.iter().any(|s| s.name == "hello" && s.kind == SymbolKind::Function)); - assert!(symbols.iter().any(|s| s.name == "User" && s.kind == SymbolKind::Class)); - assert!(symbols.iter().any(|s| s.name == "Person" && s.kind == SymbolKind::Interface)); + assert!(symbols + .iter() + .any(|s| s.name == "hello" && s.kind == SymbolKind::Function)); + assert!(symbols + .iter() + .any(|s| s.name == "User" && s.kind == SymbolKind::Class)); + assert!(symbols + .iter() + .any(|s| s.name == "Person" && s.kind == SymbolKind::Interface)); } #[test] fn test_extract_typescript_const() { let content = "export const API_URL = 'http://example.com';\nconst MAX_RETRIES = 3;\n"; let symbols = extract_typescript(content, "test.ts"); - assert!(symbols.iter().any(|s| s.name == "API_URL" && s.kind == SymbolKind::Constant)); - assert!(symbols.iter().any(|s| s.name == "MAX_RETRIES" && s.kind == SymbolKind::Constant)); + assert!(symbols + .iter() + .any(|s| s.name == "API_URL" && s.kind == SymbolKind::Constant)); + assert!(symbols + .iter() + .any(|s| s.name == "MAX_RETRIES" && s.kind == SymbolKind::Constant)); } #[test] fn test_extract_python_def_and_class() { - let content = "class MyClass:\n def method(self):\n pass\n\ndef top_func():\n pass\n"; + let content = + "class MyClass:\n def method(self):\n pass\n\ndef top_func():\n pass\n"; let symbols = extract_python(content, "test.py"); - assert!(symbols.iter().any(|s| s.name == "MyClass" && s.kind == SymbolKind::Class)); - assert!(symbols.iter().any(|s| s.name == "MyClass.method" && s.kind == SymbolKind::Function)); - assert!(symbols.iter().any(|s| s.name == "top_func" && s.kind == SymbolKind::Function)); + assert!(symbols + .iter() + .any(|s| s.name == "MyClass" && s.kind == SymbolKind::Class)); + assert!(symbols + .iter() + .any(|s| s.name == "MyClass.method" && s.kind == SymbolKind::Function)); + assert!(symbols + .iter() + .any(|s| s.name == "top_func" && s.kind == SymbolKind::Function)); } #[test] fn test_extract_python_variable() { let content = "DATABASE_URL = 'postgres://localhost'\nconfig_path = '/etc/app'\n"; let symbols = extract_python(content, "test.py"); - assert!(symbols.iter().any(|s| s.name == "DATABASE_URL" && s.kind == SymbolKind::Constant)); - assert!(symbols.iter().any(|s| s.name == "config_path" && s.kind == SymbolKind::Variable)); + assert!(symbols + .iter() + .any(|s| s.name == "DATABASE_URL" && s.kind == SymbolKind::Constant)); + assert!(symbols + .iter() + .any(|s| s.name == "config_path" && s.kind == SymbolKind::Variable)); } #[test] fn test_extract_go_func_and_struct() { let content = "func main() {}\nfunc (s *Server) Serve() {}\ntype Config struct {\n Name string\n}\n"; let symbols = extract_go(content, "test.go"); - assert!(symbols.iter().any(|s| s.name == "main" && s.kind == SymbolKind::Function)); - assert!(symbols.iter().any(|s| s.name == "Serve" && s.kind == SymbolKind::Function)); - assert!(symbols.iter().any(|s| s.name == "Config" && s.kind == SymbolKind::Struct)); + assert!(symbols + .iter() + .any(|s| s.name == "main" && s.kind == SymbolKind::Function)); + assert!(symbols + .iter() + .any(|s| s.name == "Serve" && s.kind == SymbolKind::Function)); + assert!(symbols + .iter() + .any(|s| s.name == "Config" && s.kind == SymbolKind::Struct)); } #[test] fn test_extract_go_const_and_var() { let content = "const VERSION = \"1.0\"\nvar DefaultPort = 8080\n"; let symbols = extract_go(content, "test.go"); - assert!(symbols.iter().any(|s| s.name == "VERSION" && s.kind == SymbolKind::Constant)); - assert!(symbols.iter().any(|s| s.name == "DefaultPort" && s.kind == SymbolKind::Variable)); + assert!(symbols + .iter() + .any(|s| s.name == "VERSION" && s.kind == SymbolKind::Constant)); + assert!(symbols + .iter() + .any(|s| s.name == "DefaultPort" && s.kind == SymbolKind::Variable)); } #[test] diff --git a/apps/infrastructure/src/tools/shell.rs b/apps/infrastructure/src/tools/shell.rs index 46661f3..f228d6c 100644 --- a/apps/infrastructure/src/tools/shell.rs +++ b/apps/infrastructure/src/tools/shell.rs @@ -89,9 +89,13 @@ impl Tool for Bash { let start = std::time::Instant::now(); let timeout = Duration::from_millis(timeout_ms); - let mut child_stdout = child.stdout.take() + let mut child_stdout = child + .stdout + .take() .ok_or_else(|| anyhow::anyhow!("failed to capture stdout"))?; - let mut child_stderr = child.stderr.take() + let mut child_stderr = child + .stderr + .take() .ok_or_else(|| anyhow::anyhow!("failed to capture stderr"))?; let stdout_handle = std::thread::spawn(move || -> std::io::Result> { diff --git a/apps/infrastructure/src/tools/shell_filter/credentials.rs b/apps/infrastructure/src/tools/shell_filter/credentials.rs index f22aca3..f8a760c 100644 --- a/apps/infrastructure/src/tools/shell_filter/credentials.rs +++ b/apps/infrastructure/src/tools/shell_filter/credentials.rs @@ -33,8 +33,10 @@ pub fn is_credential_path(path: &str) -> bool { /// return list of suspected credential reads. #[instrument(skip(cmd))] pub fn check_credential_read(cmd: &str) -> Vec { - let re = Regex::new(r#"(?i)(?:cat|head|tail|less|more|vim?|nano|xdg-open|open|type|echo)\s+(~?/[\w/.@-]+)"#) - .expect("hardcoded credential-read regex is valid"); + let re = Regex::new( + r#"(?i)(?:cat|head|tail|less|more|vim?|nano|xdg-open|open|type|echo)\s+(~?/[\w/.@-]+)"#, + ) + .expect("hardcoded credential-read regex is valid"); let mut findings = Vec::new(); for cap in re.captures_iter(cmd) { let path = cap.get(1).map(|m| m.as_str()).unwrap_or(""); diff --git a/apps/infrastructure/src/tools/shell_filter/git.rs b/apps/infrastructure/src/tools/shell_filter/git.rs index a7fc60c..e809a01 100644 --- a/apps/infrastructure/src/tools/shell_filter/git.rs +++ b/apps/infrastructure/src/tools/shell_filter/git.rs @@ -28,10 +28,7 @@ pub fn check_git_destructive(cmd: &str) -> Result<(), String> { for pattern in &destructive_patterns { if cmd_lower.contains(pattern) { - return Err(format!( - "destructive git operation blocked: '{}'", - pattern - )); + return Err(format!("destructive git operation blocked: '{}'", pattern)); } } diff --git a/apps/infrastructure/src/tools/util.rs b/apps/infrastructure/src/tools/util.rs index f907e49..740ad85 100644 --- a/apps/infrastructure/src/tools/util.rs +++ b/apps/infrastructure/src/tools/util.rs @@ -32,9 +32,7 @@ pub fn execute_cmd(cmd: &mut std::process::Command) -> Result { let combined = if stderr.is_empty() { stdout } else { - format!("{}\n{}", stdout, stderr) - .trim() - .to_string() + format!("{}\n{}", stdout, stderr).trim().to_string() }; let code = output.status.code().unwrap_or(-1); if output.status.success() { @@ -151,6 +149,10 @@ pub fn log_write_edit_tool( let _ = repo.append(session_dir, &mut el, entry); debug!(tool = tool_name, path = path, "edit-log entry persisted"); } else { - warn!(tool = tool_name, path = path, "failed to open edit-log repository"); + warn!( + tool = tool_name, + path = path, + "failed to open edit-log repository" + ); } } diff --git a/apps/infrastructure/src/tools/utility/todofinish.rs b/apps/infrastructure/src/tools/utility/todofinish.rs index df92a77..af4a51a 100644 --- a/apps/infrastructure/src/tools/utility/todofinish.rs +++ b/apps/infrastructure/src/tools/utility/todofinish.rs @@ -42,28 +42,33 @@ impl Tool for Todofinish { fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let item = crate::tools::arg_str(args, "item")?; info!(item, "todofinish invoked"); - + let todo_path = ctx.session_dir.join("TODO.md"); let mut content = std::fs::read_to_string(&todo_path).unwrap_or_default(); - + // Very basic replace to mark as finished let mut replaced = false; let lines: Vec<&str> = content.lines().collect(); let mut new_lines = Vec::new(); for line in lines { if line.contains(&item) && line.starts_with("- [") { - let s = line.replacen("- [high]", "- [x]", 1) - .replacen("- [medium]", "- [x]", 1) - .replacen("- [low]", "- [x]", 1); + let s = line + .replacen("- [high]", "- [x]", 1) + .replacen("- [medium]", "- [x]", 1) + .replacen("- [low]", "- [x]", 1); // if it didn't match those, just replace the first `[` - let s = if s == line { line.replacen("[ ]", "[x]", 1) } else { s }; + let s = if s == line { + line.replacen("[ ]", "[x]", 1) + } else { + s + }; new_lines.push(s); replaced = true; } else { new_lines.push(line.to_string()); } } - + if replaced { content = new_lines.join("\n") + "\n"; let _ = std::fs::write(&todo_path, &content); @@ -76,7 +81,7 @@ impl Tool for Todofinish { } else { info!(item, "TODO item not found in TODO.md — nothing to mark"); } - + Ok(format!("TODO completed: {}", item)) } } diff --git a/apps/infrastructure/src/tools/utility/todowrite.rs b/apps/infrastructure/src/tools/utility/todowrite.rs index d1888ec..cb322d5 100644 --- a/apps/infrastructure/src/tools/utility/todowrite.rs +++ b/apps/infrastructure/src/tools/utility/todowrite.rs @@ -54,11 +54,11 @@ impl Tool for Todowrite { info!(item, priority, "todowrite invoked"); let todo_line = format!("- [{}] {}\n", priority, item); - + let todo_path = ctx.session_dir.join("TODO.md"); let mut content = std::fs::read_to_string(&todo_path).unwrap_or_default(); content.push_str(&todo_line); - + let _ = std::fs::write(&todo_path, &content); if let Some(events) = &ctx.turn_events { diff --git a/apps/infrastructure/src/tools/web_search.rs b/apps/infrastructure/src/tools/web_search.rs index b643cef..c1d1e64 100644 --- a/apps/infrastructure/src/tools/web_search.rs +++ b/apps/infrastructure/src/tools/web_search.rs @@ -75,8 +75,8 @@ impl Tool for WebSearch { info!(query = %query, max_results, fetch_content, categories = %categories, "web search starting"); - let searxng_url = std::env::var("SEARXNG_URL") - .unwrap_or_else(|_| DEFAULT_SEARXNG_URL.to_string()); + let searxng_url = + std::env::var("SEARXNG_URL").unwrap_or_else(|_| DEFAULT_SEARXNG_URL.to_string()); // Build the SearXNG JSON search URL let search_url = format!( @@ -138,10 +138,7 @@ impl Tool for WebSearch { .get("title") .and_then(|v| v.as_str()) .unwrap_or("Untitled"); - let url = result - .get("url") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let url = result.get("url").and_then(|v| v.as_str()).unwrap_or(""); let snippet = result .get("content") .and_then(|v| v.as_str()) @@ -176,7 +173,9 @@ impl Tool for WebSearch { // Add suggestion for more specific queries if very few results if result_count < 3 { - output.push_str("---\n*Few results. Try a more specific query or different categories.*\n"); + output.push_str( + "---\n*Few results. Try a more specific query or different categories.*\n", + ); } Ok(output) @@ -262,10 +261,7 @@ fn fetch_page_content(url: &str, client: &reqwest::blocking::Client) -> Result>() - .join(" "); + let cleaned_text = text.split_whitespace().collect::>().join(" "); if cleaned_text.is_empty() { anyhow::bail!("no readable content found on page"); @@ -276,8 +272,5 @@ fn fetch_page_content(url: &str, client: &reqwest::blocking::Client) -> Result String { - s.lines() - .take(max_lines) - .collect::>() - .join("\n") + s.lines().take(max_lines).collect::>().join("\n") } diff --git a/apps/infrastructure/src/tools/workflow.rs b/apps/infrastructure/src/tools/workflow.rs index 878176b..a0addb6 100644 --- a/apps/infrastructure/src/tools/workflow.rs +++ b/apps/infrastructure/src/tools/workflow.rs @@ -239,13 +239,8 @@ impl Tool for HiveMind { .map(|arr| { arr.iter() .filter_map(|d| { - let directive = d - .get("directive") - .and_then(|v| v.as_str())?; - let access = d - .get("access") - .and_then(|v| v.as_str()) - .unwrap_or("read"); + let directive = d.get("directive").and_then(|v| v.as_str())?; + let access = d.get("access").and_then(|v| v.as_str()).unwrap_or("read"); Some(NodeDirective { directive: directive.to_string(), access_tier: access.to_string(), @@ -255,14 +250,17 @@ impl Tool for HiveMind { }) .unwrap_or_default(); - info!(cycle_index = cycle_idx, node_count = directives.len(), "executing hive-mind cycle"); + info!( + cycle_index = cycle_idx, + node_count = directives.len(), + "executing hive-mind cycle" + ); let cycle = CognitiveCycle { index: cycle_idx as u32, directives, }; - let nodes: Vec = - rt.block_on(async { execute_cycle(&cycle, ctx).await })?; + let nodes: Vec = rt.block_on(async { execute_cycle(&cycle, ctx).await })?; all_node_outputs.extend(nodes); } diff --git a/apps/infrastructure/src/utils.rs b/apps/infrastructure/src/utils.rs index 6b87844..53ffe49 100644 --- a/apps/infrastructure/src/utils.rs +++ b/apps/infrastructure/src/utils.rs @@ -74,7 +74,11 @@ impl CastOr for u128 { /// /// A UUID-based temporary filename avoids collisions from concurrent writes. /// Orphaned tmp files are cleaned up on any error after creation. -pub fn write_json_atomic(path: &Path, data: &T, mode: Option) -> std::io::Result<()> { +pub fn write_json_atomic( + path: &Path, + data: &T, + mode: Option, +) -> std::io::Result<()> { let tmp = path.with_extension(format!("tmp.{}", uuid::Uuid::new_v4())); let result = (|| -> std::io::Result<()> { let bytes = serde_json::to_vec_pretty(data) @@ -95,7 +99,9 @@ pub fn write_json_atomic(path: &Path, data: &T, mode: Option) std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(m))?; } #[cfg(not(unix))] - { let _ = m; } + { + let _ = m; + } } std::fs::rename(&tmp, path)?; Ok(()) @@ -160,7 +166,7 @@ pub fn build_workspace_tree(root: &Path, max_files: usize) -> String { use ignore::WalkBuilder; let mut tree = String::new(); let mut count = 0; - + // hidden(false) allows files like .github to be seen, but it still respects .gitignore // and ignores .git directories by default. for entry in WalkBuilder::new(root).hidden(false).build().flatten() { @@ -168,7 +174,7 @@ pub fn build_workspace_tree(root: &Path, max_files: usize) -> String { tree.push_str("\n... (truncated)"); break; } - + let path = entry.path(); if let Ok(rel) = path.strip_prefix(root) { let name = rel.to_string_lossy(); @@ -179,14 +185,14 @@ pub fn build_workspace_tree(root: &Path, max_files: usize) -> String { let indent = " ".repeat(depth.saturating_sub(1)); let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false); let suffix = if is_dir { "/" } else { "" }; - + let filename = entry.file_name().to_string_lossy(); tree.push_str(&format!("{indent}{filename}{suffix}\n")); count += 1; } } } - + tree.trim_end().to_string() } @@ -209,14 +215,21 @@ pub fn build_rich_context(root: &Path) -> String { let os = std::env::consts::OS; let arch = std::env::consts::ARCH; let time = chrono::Local::now().to_rfc3339(); - ctx.push_str(&format!("Environment: {} ({}), Current Time: {}\n\n", os, arch, time)); + ctx.push_str(&format!( + "Environment: {} ({}), Current Time: {}\n\n", + os, arch, time + )); // 2. Custom Rules let rule_files = [".cursorrules", ".zesdexrules", "claude.md", "agent.md"]; for file in rule_files { let p = root.join(file); if let Ok(content) = std::fs::read_to_string(&p) { - ctx.push_str(&format!("### Project Rules ({})\n```\n{}\n```\n\n", file, content.trim())); + ctx.push_str(&format!( + "### Project Rules ({})\n```\n{}\n```\n\n", + file, + content.trim() + )); } } @@ -259,7 +272,11 @@ pub fn build_rich_context(root: &Path) -> String { } else { content }; - ctx.push_str(&format!("### Tech Stack Manifest ({})\n```\n{}\n```\n\n", file, snippet.trim())); + ctx.push_str(&format!( + "### Tech Stack Manifest ({})\n```\n{}\n```\n\n", + file, + snippet.trim() + )); } } @@ -281,7 +298,10 @@ pub fn build_rich_context(root: &Path) -> String { } else { content }; - ctx.push_str(&format!("### Project Purpose (README.md)\n```\n{}\n```\n\n", snippet.trim())); + ctx.push_str(&format!( + "### Project Purpose (README.md)\n```\n{}\n```\n\n", + snippet.trim() + )); } // 7. Recent Git History & Momentum @@ -297,7 +317,10 @@ pub fn build_rich_context(root: &Path) -> String { .unwrap_or_default(); if !commits.is_empty() { - ctx.push_str(&format!("### Recent Commits (Momentum)\n```\n{}\n```\n\n", commits)); + ctx.push_str(&format!( + "### Recent Commits (Momentum)\n```\n{}\n```\n\n", + commits + )); } let diff = Command::new("git") @@ -309,7 +332,10 @@ pub fn build_rich_context(root: &Path) -> String { .unwrap_or_default(); if !diff.is_empty() { - ctx.push_str(&format!("### Recently Modified Files (Uncommitted)\n```\n{}\n```\n\n", diff)); + ctx.push_str(&format!( + "### Recently Modified Files (Uncommitted)\n```\n{}\n```\n\n", + diff + )); } } diff --git a/apps/infrastructure/src/workflow/hive_mind/cycle.rs b/apps/infrastructure/src/workflow/hive_mind/cycle.rs index 9604df5..eda2efa 100644 --- a/apps/infrastructure/src/workflow/hive_mind/cycle.rs +++ b/apps/infrastructure/src/workflow/hive_mind/cycle.rs @@ -25,10 +25,7 @@ use zesdex_domain::workflow::{CognitiveCycle, NodeOutput}; /// 3. Spawn all directives concurrently — each builds a `SubagentContext` /// and calls `run_agent` (Full access). /// 4. `try_join_all` waits for all to complete, then collect `NodeOutput`s. -pub async fn execute_cycle( - cycle: &CognitiveCycle, - tool_ctx: &ToolCtx, -) -> Result> { +pub async fn execute_cycle(cycle: &CognitiveCycle, tool_ctx: &ToolCtx) -> Result> { info!( "Executing cycle {} with {} directives", cycle.index, diff --git a/apps/infrastructure/src/workflow/hive_mind/live.rs b/apps/infrastructure/src/workflow/hive_mind/live.rs index eff3e5c..53025ca 100644 --- a/apps/infrastructure/src/workflow/hive_mind/live.rs +++ b/apps/infrastructure/src/workflow/hive_mind/live.rs @@ -23,7 +23,6 @@ impl Default for LiveHiveMind { } impl LiveHiveMind { - pub fn set_status(&self, agent_id: &str, status: &str) { if let Ok(mut guard) = self.nodes.lock() { guard.insert(agent_id.to_string(), status.to_string()); diff --git a/apps/infrastructure/src/workflow/hive_mind/synthesis.rs b/apps/infrastructure/src/workflow/hive_mind/synthesis.rs index 739b997..cc18d63 100644 --- a/apps/infrastructure/src/workflow/hive_mind/synthesis.rs +++ b/apps/infrastructure/src/workflow/hive_mind/synthesis.rs @@ -10,10 +10,7 @@ use zesdex_domain::workflow::NodeOutput; /// /// Flow: combine node outputs → return consensus text. /// Uses simple concatenation-based synthesis (avoids LLM call dependency). -pub async fn synthesize_consensus( - nodes: &[NodeOutput], - _tool_ctx: &ToolCtx, -) -> Result { +pub async fn synthesize_consensus(nodes: &[NodeOutput], _tool_ctx: &ToolCtx) -> Result { info!("Synthesizing consensus from {} nodes", nodes.len()); let mut combined = String::new(); diff --git a/apps/infrastructure/src/workflow/script.rs b/apps/infrastructure/src/workflow/script.rs index 8fa6784..caa5dc1 100644 --- a/apps/infrastructure/src/workflow/script.rs +++ b/apps/infrastructure/src/workflow/script.rs @@ -8,45 +8,45 @@ use zesdex_domain::workflow::{WorkflowPhase, WorkflowScript}; /// Parse a YAML string into a WorkflowScript. /// /// Expected format: - /// ```yaml - /// name: my-workflow - /// phases: - /// - name: research - /// directive: "Explore the codebase..." - /// - name: implement - /// directive: "Implement the changes..." - /// ``` - #[instrument] - pub fn parse_workflow_script(yaml: &str) -> Result { - let parsed: serde_json::Value = serde_yaml_ng::from_str(yaml) - .map_err(|e| anyhow::anyhow!("Failed to parse workflow YAML: {e}"))?; +/// ```yaml +/// name: my-workflow +/// phases: +/// - name: research +/// directive: "Explore the codebase..." +/// - name: implement +/// directive: "Implement the changes..." +/// ``` +#[instrument] +pub fn parse_workflow_script(yaml: &str) -> Result { + let parsed: serde_json::Value = serde_yaml_ng::from_str(yaml) + .map_err(|e| anyhow::anyhow!("Failed to parse workflow YAML: {e}"))?; - let name = parsed - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or("unnamed") - .to_string(); + let name = parsed + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("unnamed") + .to_string(); - let mut phases = Vec::new(); - if let Some(phases_arr) = parsed.get("phases").and_then(|v| v.as_array()) { - for phase_val in phases_arr { - let phase_name = phase_val - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or("phase") - .to_string(); - let directive = phase_val - .get("directive") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - phases.push(WorkflowPhase { - name: phase_name, - directive, - }); - } + let mut phases = Vec::new(); + if let Some(phases_arr) = parsed.get("phases").and_then(|v| v.as_array()) { + for phase_val in phases_arr { + let phase_name = phase_val + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("phase") + .to_string(); + let directive = phase_val + .get("directive") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + phases.push(WorkflowPhase { + name: phase_name, + directive, + }); } + } - info!("Parsed workflow script: {name} ({} phases)", phases.len()); - Ok(WorkflowScript { name, phases }) + info!("Parsed workflow script: {name} ({} phases)", phases.len()); + Ok(WorkflowScript { name, phases }) } diff --git a/apps/interfaces/api/src/error.rs b/apps/interfaces/api/src/error.rs index 6ae1a93..a530ffa 100644 --- a/apps/interfaces/api/src/error.rs +++ b/apps/interfaces/api/src/error.rs @@ -69,9 +69,7 @@ impl IntoResponse for ApiError { ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()), ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), ApiError::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()), - ApiError::TooManyRequests(msg) => { - (StatusCode::TOO_MANY_REQUESTS, msg.clone()) - } + ApiError::TooManyRequests(msg) => (StatusCode::TOO_MANY_REQUESTS, msg.clone()), ApiError::Internal(msg) => { tracing::error!(error = %msg, "Internal server error"); ( diff --git a/apps/interfaces/api/src/handlers/auth.rs b/apps/interfaces/api/src/handlers/auth.rs index 44555ef..2c76fb3 100644 --- a/apps/interfaces/api/src/handlers/auth.rs +++ b/apps/interfaces/api/src/handlers/auth.rs @@ -105,7 +105,9 @@ pub async fn login_handler( serde_json::from_str(&content) .map_err(|e| ApiError::Internal(format!("Failed to parse users: {e}")))? } else { - return Err(ApiError::Unauthorized("Invalid username or password".into())); + return Err(ApiError::Unauthorized( + "Invalid username or password".into(), + )); }; // Look up the user @@ -121,7 +123,9 @@ pub async fn login_handler( .map_err(|e| ApiError::Internal(format!("Password verification failed: {e}")))?; if !valid { - return Err(ApiError::Unauthorized("Invalid username or password".into())); + return Err(ApiError::Unauthorized( + "Invalid username or password".into(), + )); } // Generate tokens @@ -205,9 +209,7 @@ pub async fn register_handler( // Check uniqueness if users.contains_key(&req.username) { - return Err(ApiError::Conflict( - "Username already exists".into(), - )); + return Err(ApiError::Conflict("Username already exists".into())); } // Persist diff --git a/apps/interfaces/api/src/handlers/chat.rs b/apps/interfaces/api/src/handlers/chat.rs index 4b359db..0cd44b8 100644 --- a/apps/interfaces/api/src/handlers/chat.rs +++ b/apps/interfaces/api/src/handlers/chat.rs @@ -136,7 +136,8 @@ pub async fn chat_completions_handler( None, // No tool definitions for basic chat req.max_tokens, req.temperature, - ).await + ) + .await .map_err(|e| ApiError::ChatProxy(format!("LLM request failed: {e}")))?; let (prompt_tokens, completion_tokens) = usage.unwrap_or((0, 0)); diff --git a/apps/interfaces/api/src/handlers/sessions.rs b/apps/interfaces/api/src/handlers/sessions.rs index 6f09fbd..275e8de 100644 --- a/apps/interfaces/api/src/handlers/sessions.rs +++ b/apps/interfaces/api/src/handlers/sessions.rs @@ -91,8 +91,8 @@ pub async fn delete_session_handler( return Err(ApiError::BadRequest("Session ID is required".into())); } - let session_id = - SessionId::new(&id).map_err(|e| ApiError::BadRequest(format!("Invalid session ID: {e}")))?; + let session_id = SessionId::new(&id) + .map_err(|e| ApiError::BadRequest(format!("Invalid session ID: {e}")))?; state.session_service.archive_session(session_id)?; diff --git a/apps/interfaces/api/src/lib.rs b/apps/interfaces/api/src/lib.rs index d9492f2..3b77f1f 100644 --- a/apps/interfaces/api/src/lib.rs +++ b/apps/interfaces/api/src/lib.rs @@ -31,8 +31,8 @@ pub mod state; pub use error::ApiError; pub use state::ApiState; -use std::sync::Arc; use axum::Router; +use std::sync::Arc; use tower_http::cors::CorsLayer; /// Build the API router with all routes registered. @@ -64,10 +64,7 @@ pub fn build_router(state: ApiState) -> Router { Router::new() .nest("/api/v1/auth", auth_router()) .nest("/api/v1/health", health_router()) - .nest( - "/api/v1", - protected_router().layer(jwt_auth), - ) + .nest("/api/v1", protected_router().layer(jwt_auth)) .layer(cors) .with_state(shared_state) } @@ -89,7 +86,10 @@ fn protected_router() -> Router> { let sessions_router = Router::new() .route("/", axum::routing::get(sessions::list_sessions_handler)) .route("/", axum::routing::post(sessions::create_session_handler)) - .route("/{id}", axum::routing::delete(sessions::delete_session_handler)) + .route( + "/{id}", + axum::routing::delete(sessions::delete_session_handler), + ) // Conversations are sub-resources of sessions .route( "/{id}/conversations", diff --git a/apps/interfaces/api/src/middleware/auth.rs b/apps/interfaces/api/src/middleware/auth.rs index 5c3ecca..0517e74 100644 --- a/apps/interfaces/api/src/middleware/auth.rs +++ b/apps/interfaces/api/src/middleware/auth.rs @@ -100,8 +100,7 @@ where Ok(claims) => { // Reject refresh tokens on protected routes — only access // tokens are acceptable here. - if claims.token_type - != zesdex_infrastructure::auth::jwt::TokenType::Access + if claims.token_type != zesdex_infrastructure::auth::jwt::TokenType::Access { let response = ( StatusCode::UNAUTHORIZED, @@ -115,9 +114,7 @@ where } // Inject claims as extension for downstream handlers let mut req = req; - req.extensions_mut().insert(JwtClaims { - sub: claims.sub, - }); + req.extensions_mut().insert(JwtClaims { sub: claims.sub }); let fut = self.inner.call(req); return Box::pin(fut); } diff --git a/apps/interfaces/api/src/state.rs b/apps/interfaces/api/src/state.rs index 7544150..b0ca92c 100644 --- a/apps/interfaces/api/src/state.rs +++ b/apps/interfaces/api/src/state.rs @@ -169,37 +169,31 @@ pub struct ApiState { // ----------------------------------------------------------------------- // Service implementations (application-layer use cases) // ----------------------------------------------------------------------- - /// Session lifecycle management (create, list, archive). - pub session_service: - zesdex_application::auth::SessionServiceImpl< - zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository, - zesdex_infrastructure::persistence::iam::session_lock_repo::FileSystemSessionLockRepository, - >, + pub session_service: zesdex_application::auth::SessionServiceImpl< + zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository, + zesdex_infrastructure::persistence::iam::session_lock_repo::FileSystemSessionLockRepository, + >, /// Conversation message history CRUD. - pub conversation_service: - zesdex_application::cms::ConversationServiceImpl< - zesdex_infrastructure::persistence::cms::conversation_repo::JsonConversationRepository, - >, + pub conversation_service: zesdex_application::cms::ConversationServiceImpl< + zesdex_infrastructure::persistence::cms::conversation_repo::JsonConversationRepository, + >, /// Settings load/save. - pub settings_service: - zesdex_application::cms::SettingsServiceImpl< - zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository, - zesdex_infrastructure::persistence::cms::app_config_repo::JsonAppConfigRepository, - >, + pub settings_service: zesdex_application::cms::SettingsServiceImpl< + zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository, + zesdex_infrastructure::persistence::cms::app_config_repo::JsonAppConfigRepository, + >, /// Long-term memory CRUD. - pub memory_service: - zesdex_application::cms::MemoryServiceImpl< - zesdex_infrastructure::persistence::cms::memory_repo::MarkdownMemoryRepository, - >, + pub memory_service: zesdex_application::cms::MemoryServiceImpl< + zesdex_infrastructure::persistence::cms::memory_repo::MarkdownMemoryRepository, + >, // ----------------------------------------------------------------------- // Port trait implementations (infrastructure wrappers) // ----------------------------------------------------------------------- - /// Argon2id password hashing and verification. pub password_service: Argon2PasswordService, diff --git a/apps/interfaces/daemon/src/client.rs b/apps/interfaces/daemon/src/client.rs index 3f30d38..5e5ce57 100644 --- a/apps/interfaces/daemon/src/client.rs +++ b/apps/interfaces/daemon/src/client.rs @@ -15,7 +15,6 @@ use std::io::{self}; use anyhow::Result; -use zesdex_domain::SettingsRepository; use crossterm::execute; use crossterm::terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, @@ -24,6 +23,7 @@ use ratatui::backend::CrosstermBackend; use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; use ratatui::Terminal; +use zesdex_domain::SettingsRepository; use zesdex_infrastructure::ipc::client::IpcClient; use zesdex_infrastructure::ipc::protocol::{ClientRequest, DaemonFrame, StatePayload}; use zesdex_infrastructure::Toast; @@ -197,7 +197,7 @@ fn draw(frame: &mut ratatui::Frame, state: &AppStateRest) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Min(1), // main content (transcript + toasts) + Constraint::Min(1), // main content (transcript + toasts) Constraint::Length(3), // input line ]) .split(area); @@ -245,9 +245,7 @@ fn draw(frame: &mut ratatui::Frame, state: &AppStateRest) { let content = content_lines.join("\n"); - let main_block = Block::default() - .title(title) - .borders(Borders::TOP); + let main_block = Block::default().title(title).borders(Borders::TOP); let paragraph = Paragraph::new(content) .block(main_block) .wrap(Wrap { trim: false }) @@ -261,8 +259,7 @@ fn draw(frame: &mut ratatui::Frame, state: &AppStateRest) { } else { state.input.buffer.clone() }; - let input_paragraph = Paragraph::new(input_display) - .block(input_block); + let input_paragraph = Paragraph::new(input_display).block(input_block); frame.render_widget(input_paragraph, chunks[1]); // Set cursor position for the input line. @@ -347,10 +344,7 @@ pub fn run_attach(session_id: &str) -> Result<()> { client.send(&ClientRequest::Tick)?; } - handle_daemon_frame( - &mut client_state, - client.receive::()?, - ); + handle_daemon_frame(&mut client_state, client.receive::()?); terminal.draw(|f| { draw(f, &client_state); diff --git a/apps/interfaces/daemon/src/handler.rs b/apps/interfaces/daemon/src/handler.rs index dad7ea7..3f1d29d 100644 --- a/apps/interfaces/daemon/src/handler.rs +++ b/apps/interfaces/daemon/src/handler.rs @@ -15,7 +15,6 @@ //! 4. After each request, `send_daemon_update` pushes a full state //! snapshot back to the client - use anyhow::Result; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use tracing::info; @@ -27,9 +26,7 @@ use zesdex_infrastructure::ipc::protocol::{ use zesdex_infrastructure::utils::CastOr; use crate::key_code::key_action_to_code; -use crate::state::{ - AppStateRest, AutocompleteKind, ChatMessageDisplay, Overlay, RoleWrapper, -}; +use crate::state::{AppStateRest, AutocompleteKind, ChatMessageDisplay, Overlay, RoleWrapper}; // --------------------------------------------------------------------------- // Action enum @@ -80,30 +77,17 @@ pub enum Action { /// Periodic timer tick — drains queued events and runs side jobs. Tick, /// Accept a lesson by name. - LessonAccept { - name: String, - }, + LessonAccept { name: String }, /// Reject a lesson by name. - LessonReject { - name: String, - }, + LessonReject { name: String }, /// Delete a previously stored lesson by name. - LessonDelete { - name: String, - }, + LessonDelete { name: String }, /// Start the OAuth device-code login flow for a named provider. - StartOAuth { - provider: String, - }, + StartOAuth { provider: String }, /// Open the inline file editor for `path`. - OpenEditor { - path: String, - }, + OpenEditor { path: String }, /// Register a new MCP server by name and shell command. - McpAdd { - name: String, - command: String, - }, + McpAdd { name: String, command: String }, /// Open the model-picker overlay. ModelList, /// Set the abort flag on the currently running turn. @@ -149,9 +133,10 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { Action::CloseOverlay => handle_close_overlay(state), // ── System / info ───────────────────────────────────────────── - Action::SystemNote { kind: _kind, message } => { - handle_system_note(state, message) - } + Action::SystemNote { + kind: _kind, + message, + } => handle_system_note(state, message), Action::ModelList => handle_model_list(state), Action::AbortTurn => handle_abort_turn(state), Action::Compact => handle_compact(state), @@ -228,7 +213,10 @@ fn handle_tick(state: &mut AppStateRest) { TurnEvent::Error(e) => { state.toast_error(e); } - TurnEvent::Usage { tokens_in, tokens_out } => { + TurnEvent::Usage { + tokens_in, + tokens_out, + } => { if let Some(ref mut rt) = state.session_runtime { rt.usage.tokens_in = rt.usage.tokens_in.saturating_add(tokens_in); rt.usage.tokens_out = rt.usage.tokens_out.saturating_add(tokens_out); @@ -458,7 +446,9 @@ fn handle_model_list(state: &mut AppStateRest) { } fn handle_abort_turn(state: &mut AppStateRest) { - state.abort_flag.store(true, std::sync::atomic::Ordering::SeqCst); + state + .abort_flag + .store(true, std::sync::atomic::Ordering::SeqCst); if let Ok(mut in_flight) = state.turn_in_flight.lock() { *in_flight = false; } @@ -469,15 +459,26 @@ fn handle_compact(state: &mut AppStateRest) { tracing::info!("compacting conversation with AI summarization"); let provider_name = &state.settings.provider; let provider_cfg = state.app_config.providers.get(provider_name).cloned(); - let api_key = state.settings.api_keys.get(provider_name).cloned().unwrap_or_default(); + let api_key = state + .settings + .api_keys + .get(provider_name) + .cloned() + .unwrap_or_default(); let model = state.settings.model.clone(); let api_base = provider_cfg.map(|cfg| cfg.api_base.clone()); let client = zesdex_infrastructure::llm::provider::LlmClient::new(api_key, model, api_base); if let Some(ref mut rt) = state.session_runtime { - let tokio_rt = tokio::runtime::Runtime::new().expect("create tokio runtime for AI compaction"); - if let Ok(()) = tokio_rt.block_on(zesdex_application::agent::turn_service::compact_messages_with_ai(&mut rt.messages, &client)) { + let tokio_rt = + tokio::runtime::Runtime::new().expect("create tokio runtime for AI compaction"); + if let Ok(()) = tokio_rt.block_on( + zesdex_application::agent::turn_service::compact_messages_with_ai( + &mut rt.messages, + &client, + ), + ) { let msg_count = rt.messages.len(); state.push_transcript(ChatMessageDisplay::new( RoleWrapper::System, @@ -619,8 +620,8 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { state.input.open_autocomplete(); state.dirty = true; } else { - state.input.autocomplete_idx = - (state.input.autocomplete_idx + 1) % state.input.autocomplete_candidates.len().max(1); + state.input.autocomplete_idx = (state.input.autocomplete_idx + 1) + % state.input.autocomplete_candidates.len().max(1); state.dirty = true; } vec![] @@ -638,11 +639,12 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { } else if state.input.autocomplete_visible { // Select the current autocomplete candidate if !state.input.autocomplete_candidates.is_empty() { - let idx = state.input.autocomplete_idx + let idx = state + .input + .autocomplete_idx .min(state.input.autocomplete_candidates.len().saturating_sub(1)); if state.input.autocomplete_kind == AutocompleteKind::Command { - state.input.buffer = - state.input.autocomplete_candidates[idx].clone(); + state.input.buffer = state.input.autocomplete_candidates[idx].clone(); state.input.cursor = state.input.buffer.len(); } state.input.close_autocomplete(); @@ -778,10 +780,7 @@ pub fn send_daemon_update(conn: &mut Connection, state: &AppStateRest) -> Result /// Handle an incoming client connection for the daemon. /// /// Flow: loop reading requests, modifying state, and sending updates back. -pub fn handle_daemon_client( - mut conn: Connection, - state: &mut AppStateRest, -) -> Result<()> { +pub fn handle_daemon_client(mut conn: Connection, state: &mut AppStateRest) -> Result<()> { tracing::debug!("handling daemon client connection"); let mut running = true; while running { @@ -807,8 +806,7 @@ pub fn handle_daemon_client( if shift { modifiers |= KeyModifiers::SHIFT; } - let key_event = - KeyEvent::new(key_action_to_code(&key), modifiers); + let key_event = KeyEvent::new(key_action_to_code(&key), modifiers); let actions = handle_key(key_event, state); for action in actions { apply_action(state, action); @@ -817,10 +815,7 @@ pub fn handle_daemon_client( } ClientRequest::Submit(text) => { state.input.buffer = text; - let enter_event = KeyEvent::new( - KeyCode::Enter, - KeyModifiers::NONE, - ); + let enter_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE); let actions = handle_key(enter_event, state); for action in actions { apply_action(state, action); diff --git a/apps/interfaces/daemon/src/state.rs b/apps/interfaces/daemon/src/state.rs index b907b61..6b58ab9 100644 --- a/apps/interfaces/daemon/src/state.rs +++ b/apps/interfaces/daemon/src/state.rs @@ -16,19 +16,19 @@ use anyhow::Result; use tokio::sync::RwLock; use zesdex_domain::cms::EditLog; -use zesdex_domain::Session; -use zesdex_domain::Settings; use zesdex_domain::AppConfigRepository; use zesdex_domain::EditLogRepository; +use zesdex_domain::Session; use zesdex_domain::SessionLockRepository; use zesdex_domain::SessionRepository; +use zesdex_domain::Settings; use zesdex_domain::SettingsRepository; use zesdex_infrastructure::lsp::manager::LspManager; use zesdex_infrastructure::mcp::manager::McpManager; use zesdex_infrastructure::persistence::FileSystemSessionLockRepository; use zesdex_infrastructure::persistence::JsonAppConfigRepository; -use zesdex_infrastructure::persistence::JsonlEditLogRepository; use zesdex_infrastructure::persistence::JsonSettingsRepository; +use zesdex_infrastructure::persistence::JsonlEditLogRepository; use zesdex_infrastructure::AppConfig; use zesdex_infrastructure::DirCache; use zesdex_infrastructure::MentionIndex; @@ -375,9 +375,7 @@ pub struct WorkflowEngine { impl WorkflowEngine { /// Create an empty workflow engine. pub fn new() -> Self { - Self { - agents: Vec::new(), - } + Self { agents: Vec::new() } } } @@ -487,8 +485,7 @@ impl AppStateRest { session_dir: &std::path::Path, memory_dir: PathBuf, ) -> Self { - let store_base_dir = - zesdex_infrastructure::Store::new().base_dir; + let store_base_dir = zesdex_infrastructure::Store::new().base_dir; let settings = JsonSettingsRepository::new() .load(&store_base_dir) .unwrap_or_default(); @@ -705,8 +702,7 @@ impl AppStateRest { /// Persist the current settings to the store and swallow any error. pub fn save_settings(&self) { - let _ = JsonSettingsRepository::new() - .save(&self.store_base_dir(), &self.settings); + let _ = JsonSettingsRepository::new().save(&self.store_base_dir(), &self.settings); } } @@ -807,8 +803,7 @@ pub fn create_session() -> Result<( let workspace_roots = vec![std::env::current_dir()?]; let mut state = AppStateRest::new(workspace_roots, &session_dir, store.memory_dir.clone()); state.spawn_mention_index_build(); - let session_repo = - zesdex_infrastructure::persistence::FileSystemSessionRepository::new(); + let session_repo = zesdex_infrastructure::persistence::FileSystemSessionRepository::new(); state.sessions = session_repo .list_sessions(&store.base_dir) .unwrap_or_default(); diff --git a/apps/interfaces/tui/src/action/handlers.rs b/apps/interfaces/tui/src/action/handlers.rs index 40953df..539c3bf 100644 --- a/apps/interfaces/tui/src/action/handlers.rs +++ b/apps/interfaces/tui/src/action/handlers.rs @@ -53,8 +53,7 @@ fn apply_turn_event(state: &mut AppStateRest, event: zesdex_infrastructure::Turn rt.hive_mind_converged = true; } } else { - state - .push_transcript(ChatMessageDisplay::new(Role::System, message)); + state.push_transcript(ChatMessageDisplay::new(Role::System, message)); } } zesdex_infrastructure::TurnEvent::AssistantMessage(msg) => { @@ -82,8 +81,7 @@ fn apply_turn_event(state: &mut AppStateRest, event: zesdex_infrastructure::Turn state.toast_error(msg); } zesdex_infrastructure::TurnEvent::StreamStart => { - state - .push_transcript(ChatMessageDisplay::new(Role::Assistant, String::new())); + state.push_transcript(ChatMessageDisplay::new(Role::Assistant, String::new())); } zesdex_infrastructure::TurnEvent::StreamToken(text) => { state.append_to_last_transcript(&text, false); @@ -150,7 +148,9 @@ fn apply_workflow_update( state .workflow_engine .agents - .push(crate::state::SimpleAgent::with_display(agent_id, agent_name)); + .push(crate::state::SimpleAgent::with_display( + agent_id, agent_name, + )); } } zesdex_infrastructure::AgentStatus::Running => { @@ -160,8 +160,7 @@ fn apply_workflow_update( state.workflow_engine.agents[i].display_name = agent_name; state.workflow_engine.agents[i].started_at = Some(now); } else { - let mut agent = - crate::state::SimpleAgent::with_display(agent_id, agent_name); + let mut agent = crate::state::SimpleAgent::with_display(agent_id, agent_name); agent.state = crate::state::AgentState::Running; agent.started_at = Some(now); state.workflow_engine.agents.push(agent); diff --git a/apps/interfaces/tui/src/action/mod.rs b/apps/interfaces/tui/src/action/mod.rs index 04d68e4..476ef8f 100644 --- a/apps/interfaces/tui/src/action/mod.rs +++ b/apps/interfaces/tui/src/action/mod.rs @@ -122,8 +122,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { // ── Input ────────────────────────────────────────────────────── Action::SubmitInput(text) => { - state - .push_transcript(ChatMessageDisplay::new(Role::User, text.clone())); + state.push_transcript(ChatMessageDisplay::new(Role::User, text.clone())); state.input.submit(); crate::turn::spawn_agent_turn(state, text); state.token_count_dirty = true; @@ -176,8 +175,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { state.mark_dirty(); } Action::SystemNote { kind: _, message } => { - state - .push_transcript(ChatMessageDisplay::new(Role::System, message)); + state.push_transcript(ChatMessageDisplay::new(Role::System, message)); } Action::ModelList => { state.misc.overlay = Overlay::ModelSelector; @@ -226,12 +224,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { state.mark_dirty(); } Action::DiffScroll(amount) => { - let max_scroll = state - .misc - .diff_content - .lines() - .count() - .saturating_sub(1); + let max_scroll = state.misc.diff_content.lines().count().saturating_sub(1); let new_scroll = (state.misc.diff_scroll as i32 + amount).max(0) as usize; state.misc.diff_scroll = new_scroll.min(max_scroll); state.mark_dirty(); diff --git a/apps/interfaces/tui/src/components/component.rs b/apps/interfaces/tui/src/components/component.rs index f1b232f..9fb8867 100644 --- a/apps/interfaces/tui/src/components/component.rs +++ b/apps/interfaces/tui/src/components/component.rs @@ -1,10 +1,10 @@ //! Base trait for all TUI components. +use crate::action::Action; +use crate::state::AppStateRest; use crossterm::event::Event; use ratatui::layout::Rect; use ratatui::Frame; -use crate::action::Action; -use crate::state::AppStateRest; /// Base trait for modular UI components in the TUI. pub trait Component { @@ -13,7 +13,7 @@ pub trait Component { /// Optional: Pre-render caching phase called before draw. fn pre_render(&mut self, _state: &mut AppStateRest) {} - + /// Handle an input event. Return an optional Action to dispatch. fn handle_event(&mut self, _event: Event, _state: &mut AppStateRest) -> Option { None diff --git a/apps/interfaces/tui/src/controller/command.rs b/apps/interfaces/tui/src/controller/command.rs index 07bfa06..154cd7d 100644 --- a/apps/interfaces/tui/src/controller/command.rs +++ b/apps/interfaces/tui/src/controller/command.rs @@ -104,13 +104,17 @@ pub fn parse_command(text: &str) -> Command { pub fn apply_command(cmd: Command) -> Vec { match cmd { Command::Help => { - vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Help)] + vec![crate::action::Action::OpenOverlay( + crate::state::Overlay::Help, + )] } Command::Quit => { vec![crate::action::Action::QuitConfirm] } Command::McpOpen => { - vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Mcp)] + vec![crate::action::Action::OpenOverlay( + crate::state::Overlay::Mcp, + )] } Command::Clear => { vec![crate::action::Action::SystemNote { @@ -119,7 +123,9 @@ pub fn apply_command(cmd: Command) -> Vec { }] } Command::ClearConfirm => { - vec![crate::action::Action::OpenOverlay(crate::state::Overlay::ClearConfirm)] + vec![crate::action::Action::OpenOverlay( + crate::state::Overlay::ClearConfirm, + )] } Command::Login { provider } => { vec![crate::action::Action::StartOAuth { provider }] @@ -137,13 +143,19 @@ pub fn apply_command(cmd: Command) -> Vec { vec![crate::action::Action::Compact] } Command::TodoOpen => { - vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Todo)] + vec![crate::action::Action::OpenOverlay( + crate::state::Overlay::Todo, + )] } Command::PlanOpen => { - vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Plan)] + vec![crate::action::Action::OpenOverlay( + crate::state::Overlay::Plan, + )] } Command::UsageOpen => { - vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Usage)] + vec![crate::action::Action::OpenOverlay( + crate::state::Overlay::Usage, + )] } Command::Diff => { vec![crate::action::Action::ShowDiff] diff --git a/apps/interfaces/tui/src/controller/input.rs b/apps/interfaces/tui/src/controller/input.rs index f8d6226..039747e 100644 --- a/apps/interfaces/tui/src/controller/input.rs +++ b/apps/interfaces/tui/src/controller/input.rs @@ -16,7 +16,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}; +use crate::state::{AppStateRest, AutocompleteKind, Overlay}; /// Mark state dirty and return an empty action list. fn mark(state: &mut AppStateRest) -> Vec { @@ -120,13 +120,15 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { KeyCode::Up => { let items = crate::state::get_learning_items(state); let n = items.len(); - state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, false); + state.misc.selected_index = + crate::state::cycle_selected_index(state.misc.selected_index, n, false); return mark(state); } KeyCode::Down => { let items = crate::state::get_learning_items(state); let n = items.len(); - state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, true); + state.misc.selected_index = + crate::state::cycle_selected_index(state.misc.selected_index, n, true); return mark(state); } KeyCode::Enter | KeyCode::Char('a') => { @@ -237,12 +239,14 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { Vec::new() } else if state.misc.overlay == Overlay::Rewind { let n = crate::state::rewind_count(state); - state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, false); + state.misc.selected_index = + crate::state::cycle_selected_index(state.misc.selected_index, n, false); state.mark_dirty(); Vec::new() } else if state.misc.overlay == Overlay::ModelSelector { let n = state.app_config.providers.len(); - state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, false); + state.misc.selected_index = + crate::state::cycle_selected_index(state.misc.selected_index, n, false); state.mark_dirty(); Vec::new() } else if key.modifiers.contains(KeyModifiers::CONTROL) { @@ -261,12 +265,14 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { Vec::new() } else if state.misc.overlay == Overlay::Rewind { let n = crate::state::rewind_count(state); - state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, true); + state.misc.selected_index = + crate::state::cycle_selected_index(state.misc.selected_index, n, true); state.mark_dirty(); Vec::new() } else if state.misc.overlay == Overlay::ModelSelector { let n = state.app_config.providers.len(); - state.misc.selected_index = crate::state::cycle_selected_index(state.misc.selected_index, n, true); + state.misc.selected_index = + crate::state::cycle_selected_index(state.misc.selected_index, n, true); state.mark_dirty(); Vec::new() } else if key.modifiers.contains(KeyModifiers::CONTROL) { diff --git a/apps/interfaces/tui/src/controller/overlay_enter.rs b/apps/interfaces/tui/src/controller/overlay_enter.rs index 6d7a7ab..58a5c1e 100644 --- a/apps/interfaces/tui/src/controller/overlay_enter.rs +++ b/apps/interfaces/tui/src/controller/overlay_enter.rs @@ -95,7 +95,10 @@ fn handle_model_selector_enter(state: &mut AppStateRest) -> Vec { 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()); + state + .settings + .api_keys + .insert(provider.clone(), key.clone()); } else if let Some(env_key) = cfg .api_key_env .as_ref() diff --git a/apps/interfaces/tui/src/lib.rs b/apps/interfaces/tui/src/lib.rs index c012d65..ca6bbfa 100644 --- a/apps/interfaces/tui/src/lib.rs +++ b/apps/interfaces/tui/src/lib.rs @@ -68,12 +68,11 @@ pub mod view; // Re-exports for convenient access by consumers (main.rs / bin entry points) // --------------------------------------------------------------------------- -pub use action::{Action, apply_action}; +pub use action::{apply_action, Action}; pub use run::run_single_process; pub use state::{ - AgentState, AppStateRest, AutocompleteKind, ChatMessageDisplay, InputState, - MiscState, Overlay, ScrollState, SimpleAgent, SimpleWorkflowEngine, - TranscriptCache, EditorState, + AgentState, AppStateRest, AutocompleteKind, ChatMessageDisplay, EditorState, InputState, + MiscState, Overlay, ScrollState, SimpleAgent, SimpleWorkflowEngine, TranscriptCache, }; /// Convenience: initialise a `Store` for data directory resolution. diff --git a/apps/interfaces/tui/src/run.rs b/apps/interfaces/tui/src/run.rs index a7e2668..9d3742e 100644 --- a/apps/interfaces/tui/src/run.rs +++ b/apps/interfaces/tui/src/run.rs @@ -8,9 +8,13 @@ //! restore terminal → save settings → release lock. use anyhow::Result; +use crossterm::event::{ + DisableBracketedPaste, DisableMouseCapture, Event, KeyEventKind, MouseEventKind, +}; use crossterm::execute; -use crossterm::event::{DisableBracketedPaste, DisableMouseCapture, Event, KeyEventKind, MouseEventKind}; -use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, +}; use ratatui::backend::CrosstermBackend; use ratatui::Terminal; use std::io::{self, Write}; @@ -126,9 +130,9 @@ fn run_loop_inner( // Adaptive poll: fast when animating, slow when idle. let poll_timeout = if state.turn_in_flight() { - Duration::from_millis(50) // smooth spinner @ ~20fps + Duration::from_millis(50) // smooth spinner @ ~20fps } else { - Duration::from_millis(200) // idle: 5fps, saves CPU + Duration::from_millis(200) // idle: 5fps, saves CPU }; if crossterm::event::poll(poll_timeout)? { @@ -140,7 +144,8 @@ fn run_loop_inner( apply_action(state, action); } if let Some(text) = state.misc.pending_clipboard_copy.take() { - let _ = zesdex_infrastructure::utils::write_osc52(&mut io::stdout(), &text); + let _ = + zesdex_infrastructure::utils::write_osc52(&mut io::stdout(), &text); state.push_toast(zesdex_infrastructure::Toast::new( zesdex_infrastructure::ToastKind::Success, "Copied to clipboard".to_string(), @@ -195,13 +200,15 @@ fn create_local_session() -> Result<(zesdex_domain::core::Store, AppStateRest)> // Load real settings from disk use zesdex_domain::cms::{AppConfigRepository, SettingsRepository}; - let settings = zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository::new() - .load(&store.base_dir) - .unwrap_or_default(); + let settings = + zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository::new() + .load(&store.base_dir) + .unwrap_or_default(); - let app_config = zesdex_infrastructure::persistence::cms::app_config_repo::JsonAppConfigRepository::new() - .load(&store.base_dir) - .unwrap_or_default(); + let app_config = + zesdex_infrastructure::persistence::cms::app_config_repo::JsonAppConfigRepository::new() + .load(&store.base_dir) + .unwrap_or_default(); let workspace_roots = vec![std::env::current_dir()?]; let mut state = AppStateRest::new(workspace_roots, &session_dir, store.memory_dir.clone()); diff --git a/apps/interfaces/tui/src/state/input.rs b/apps/interfaces/tui/src/state/input.rs index 4420dd8..c2088b9 100644 --- a/apps/interfaces/tui/src/state/input.rs +++ b/apps/interfaces/tui/src/state/input.rs @@ -13,9 +13,20 @@ pub enum AutocompleteKind { /// Builtin slash-commands recognised by the chat input autocomplete. const COMMANDS: &[&str] = &[ - "/help", "/quit", "/clear", "/login", "/login zen", "/login openai", - "/edit", "/mcp add", "/model", "/model ls", "/model add", - "/todo", "/usage", "/compact", + "/help", + "/quit", + "/clear", + "/login", + "/login zen", + "/login openai", + "/edit", + "/mcp add", + "/model", + "/model ls", + "/model add", + "/todo", + "/usage", + "/compact", ]; /// The user's input buffer, cursor position, history, and autocomplete diff --git a/apps/interfaces/tui/src/state/misc.rs b/apps/interfaces/tui/src/state/misc.rs index 80f9a64..6e4bccb 100644 --- a/apps/interfaces/tui/src/state/misc.rs +++ b/apps/interfaces/tui/src/state/misc.rs @@ -96,7 +96,11 @@ 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 } + EditorState { + path, + content, + cursor, + } } /// Return the full buffer content. diff --git a/apps/interfaces/tui/src/state/mod.rs b/apps/interfaces/tui/src/state/mod.rs index d02f776..363d2e3 100644 --- a/apps/interfaces/tui/src/state/mod.rs +++ b/apps/interfaces/tui/src/state/mod.rs @@ -25,8 +25,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tracing::warn; -use zesdex_domain::cms::{AppConfig, Settings}; use ratatui::text::Line; +use zesdex_domain::cms::{AppConfig, Settings}; use zesdex_infrastructure::{DirCache, MentionIndex, SessionRuntime, Toast, TurnEvent}; pub mod helpers; @@ -47,7 +47,7 @@ pub use workflow::{AgentState, SimpleAgent, SimpleWorkflowEngine}; // Re-export the most commonly used helpers at the `state` level. pub use helpers::{ count_tokens, current_effort, cycle_effort, cycle_selected_index, get_learning_items, - rewind_count, resolve_context_window, EFFORT_LEVELS, LearningItem, + resolve_context_window, rewind_count, LearningItem, EFFORT_LEVELS, }; // --------------------------------------------------------------------------- @@ -194,10 +194,7 @@ impl AppStateRest { ) -> Self { let settings = Settings::default(); let app_config = AppConfig::default(); - let worktrees_dir = memory_dir - .parent() - .unwrap_or(&memory_dir) - .join("worktrees"); + let worktrees_dir = memory_dir.parent().unwrap_or(&memory_dir).join("worktrees"); let session_id = session_dir.file_name().map_or_else( || { warn!("[state] session_dir has no file_name, using empty session_id"); @@ -272,29 +269,42 @@ impl AppStateRest { /// Push an info toast. pub fn toast_info(&mut self, msg: impl Into) { - self.push_toast(Toast::new(zesdex_infrastructure::ToastKind::Info, msg.into())); + self.push_toast(Toast::new( + zesdex_infrastructure::ToastKind::Info, + msg.into(), + )); } /// Push a success toast. pub fn toast_success(&mut self, msg: impl Into) { - self.push_toast(Toast::new(zesdex_infrastructure::ToastKind::Success, msg.into())); + self.push_toast(Toast::new( + zesdex_infrastructure::ToastKind::Success, + msg.into(), + )); } /// Push a warning toast. pub fn toast_warning(&mut self, msg: impl Into) { - self.push_toast(Toast::new(zesdex_infrastructure::ToastKind::Warning, msg.into())); + self.push_toast(Toast::new( + zesdex_infrastructure::ToastKind::Warning, + msg.into(), + )); } /// Push an error toast. pub fn toast_error(&mut self, msg: impl Into) { - self.push_toast(Toast::new(zesdex_infrastructure::ToastKind::Error, msg.into())); + self.push_toast(Toast::new( + zesdex_infrastructure::ToastKind::Error, + msg.into(), + )); } /// Persist settings to disk. pub fn save_settings(&self) { if let Ok(store_dir) = std::fs::canonicalize(self.store_base_dir()) { let repo = - zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository::new(); + zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository::new( + ); use zesdex_domain::SettingsRepository; if let Err(e) = repo.save(&store_dir, &self.settings) { tracing::warn!("Failed to save settings: {e}"); diff --git a/apps/interfaces/tui/src/turn.rs b/apps/interfaces/tui/src/turn.rs index 0ffade2..8fd81b4 100644 --- a/apps/interfaces/tui/src/turn.rs +++ b/apps/interfaces/tui/src/turn.rs @@ -4,13 +4,13 @@ use std::sync::atomic::Ordering; use tracing::info; -use zesdex_domain::core::ChatMessage; +use zesdex_application::agent::turn_service::AgentTurnServiceImpl; +use zesdex_application::agent::AgentTurnService; use zesdex_domain::agent::AgentTurnParams; +use zesdex_domain::core::ChatMessage; use zesdex_infrastructure::llm::provider::LlmClient; use zesdex_infrastructure::tools::executor::InfrastructureToolExecutor; use zesdex_infrastructure::tools::{all_tools, tool_defs, ToolCtx}; -use zesdex_application::agent::turn_service::AgentTurnServiceImpl; -use zesdex_application::agent::AgentTurnService; use crate::state::AppStateRest; @@ -113,7 +113,9 @@ pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) { // Clone credentials before moving into LlmClient. let explore_api_key = api_key.clone(); let explore_model = model.clone(); - let explore_base_url = api_base.clone().unwrap_or_else(|| "https://api.openai.com/v1".to_string()); + let explore_base_url = api_base + .clone() + .unwrap_or_else(|| "https://api.openai.com/v1".to_string()); let client = std::sync::Arc::new(LlmClient::new(api_key, model, api_base)); @@ -126,8 +128,7 @@ pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) { // Clone ToolCtx for the explore service (before moving into executor). let explore_ctx = tool_ctx.clone(); - let tool_executor = - std::sync::Arc::new(InfrastructureToolExecutor::new(tool_ctx)); + let tool_executor = std::sync::Arc::new(InfrastructureToolExecutor::new(tool_ctx)); let tools = all_tools(); let defs = tool_defs(&tools); @@ -145,8 +146,8 @@ pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) { ), ); - let turn_service = AgentTurnServiceImpl::new(client, tool_executor, defs) - .with_explore(explore_service); + let turn_service = + AgentTurnServiceImpl::new(client, tool_executor, defs).with_explore(explore_service); tokio::spawn(async move { let _ = turn_service.run_turn(params).await; diff --git a/apps/interfaces/tui/src/view/chat.rs b/apps/interfaces/tui/src/view/chat.rs index eb84f8e..840e581 100644 --- a/apps/interfaces/tui/src/view/chat.rs +++ b/apps/interfaces/tui/src/view/chat.rs @@ -96,12 +96,16 @@ fn render_one_message( if !msg.reasoning.trim().is_empty() { let reasoning_str = format!("[Thinking...]\n{}", msg.reasoning.trim()); - let reasoning_spans = super::markdown::render_markdown(&reasoning_str, content_width, false); + let reasoning_spans = + super::markdown::render_markdown(&reasoning_str, content_width, false); // Dim the reasoning text - let dimmed_spans: Vec = reasoning_spans.into_iter().map(|mut s| { - s.style = s.style.fg(Theme::TEXT_DIM); - s - }).collect(); + let dimmed_spans: Vec = reasoning_spans + .into_iter() + .map(|mut s| { + s.style = s.style.fg(Theme::TEXT_DIM); + s + }) + .collect(); let reasoning_lines = split_spans_into_lines(dimmed_spans); let mut lines_iter = reasoning_lines.into_iter(); @@ -156,7 +160,7 @@ fn render_one_message( lines.push(Line::from(spans)); } } - + if !first_line_handled { lines.push(Line::from(header_prefix)); } @@ -166,134 +170,136 @@ fn render_one_message( impl Component for ChatComponent { fn pre_render(&mut self, state: &mut crate::state::AppStateRest) { - let content_width = state.last_render_width.saturating_sub(PREFIX_WIDTH as u16 + 2); - let msg_count = state.transcript_cache.messages.len(); - let cached_count = state.cached_msg_count; + let content_width = state + .last_render_width + .saturating_sub(PREFIX_WIDTH as u16 + 2); + let msg_count = state.transcript_cache.messages.len(); + let cached_count = state.cached_msg_count; - let needs_full_rebuild = state.transcript_cache.dirty - && (state.last_render_width == 0 + let needs_full_rebuild = state.transcript_cache.dirty + && (state.last_render_width == 0 || msg_count < cached_count // pesan di-evict dari depan || state.render_width_at_cache != state.last_render_width); // resize - if needs_full_rebuild { - // Full rebuild: parse semua pesan dari nol - let mut all_lines: Vec> = Vec::new(); - for msg in &state.transcript_cache.messages { - let msg_lines = render_one_message(msg, content_width); - all_lines.extend(msg_lines); - } - state.display_lines_cache = all_lines; - state.cached_msg_count = msg_count; - state.render_width_at_cache = state.last_render_width; - state.transcript_cache.dirty = false; - } else if state.transcript_cache.dirty && msg_count > cached_count { - // Incremental: hanya render pesan baru yang belum ada di cache - let new_msgs: Vec<_> = state - .transcript_cache - .messages - .iter() - .skip(cached_count) - .collect(); - for msg in new_msgs { - let msg_lines = render_one_message(msg, content_width); - state.display_lines_cache.extend(msg_lines); - } - state.cached_msg_count = msg_count; - state.transcript_cache.dirty = false; - } - - // Lazily recompute token count — hanya saat ada pesan baru - if state.token_count_dirty { - if let Some(ref rt) = state.session_runtime { - state.cached_token_count = rt + if needs_full_rebuild { + // Full rebuild: parse semua pesan dari nol + let mut all_lines: Vec> = Vec::new(); + for msg in &state.transcript_cache.messages { + let msg_lines = render_one_message(msg, content_width); + all_lines.extend(msg_lines); + } + state.display_lines_cache = all_lines; + state.cached_msg_count = msg_count; + state.render_width_at_cache = state.last_render_width; + state.transcript_cache.dirty = false; + } else if state.transcript_cache.dirty && msg_count > cached_count { + // Incremental: hanya render pesan baru yang belum ada di cache + let new_msgs: Vec<_> = state + .transcript_cache .messages .iter() - .filter_map(|m| m.content.as_deref()) - .map(crate::state::count_tokens) - .sum(); - } else { - state.cached_token_count = 0; + .skip(cached_count) + .collect(); + for msg in new_msgs { + let msg_lines = render_one_message(msg, content_width); + state.display_lines_cache.extend(msg_lines); + } + state.cached_msg_count = msg_count; + state.transcript_cache.dirty = false; + } + + // Lazily recompute token count — hanya saat ada pesan baru + if state.token_count_dirty { + if let Some(ref rt) = state.session_runtime { + state.cached_token_count = rt + .messages + .iter() + .filter_map(|m| m.content.as_deref()) + .map(crate::state::count_tokens) + .sum(); + } else { + state.cached_token_count = 0; + } + state.token_count_dirty = false; } - state.token_count_dirty = false; } -} fn draw(&mut self, frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest) { - let messages = &state.transcript_cache.messages; - let scroll_offset = state.scroll.offset; - let max_visible = (area.height as usize).saturating_sub(3); - let cache = &state.display_lines_cache; + let messages = &state.transcript_cache.messages; + let scroll_offset = state.scroll.offset; + let max_visible = (area.height as usize).saturating_sub(3); + let cache = &state.display_lines_cache; - // Hitung window tanpa clone - let total = cache.len(); - let spinner_extra = usize::from(state.turn_in_flight()); - let total_with_spinner = total + spinner_extra; - let max_offset = total_with_spinner.saturating_sub(max_visible); - let offset = scroll_offset.min(max_offset); + // Hitung window tanpa clone + let total = cache.len(); + let spinner_extra = usize::from(state.turn_in_flight()); + let total_with_spinner = total + spinner_extra; + let max_offset = total_with_spinner.saturating_sub(max_visible); + let offset = scroll_offset.min(max_offset); - let end_idx = total_with_spinner.saturating_sub(offset); - let start_idx = end_idx.saturating_sub(max_visible); + let end_idx = total_with_spinner.saturating_sub(offset); + let start_idx = end_idx.saturating_sub(max_visible); - // Kumpulkan hanya baris yang visible — tidak clone semua - let mut visible: Vec = Vec::with_capacity(max_visible); - let cache_end = end_idx.min(total); - let cache_start = start_idx.min(cache_end); - if cache_start < cache_end { - visible.extend_from_slice(&cache[cache_start..cache_end]); - } + // Kumpulkan hanya baris yang visible — tidak clone semua + let mut visible: Vec = Vec::with_capacity(max_visible); + let cache_end = end_idx.min(total); + let cache_start = start_idx.min(cache_end); + if cache_start < cache_end { + visible.extend_from_slice(&cache[cache_start..cache_end]); + } - // Spinner hanya ditambahkan jika visible window mencakup posisi terakhir - if state.turn_in_flight() && end_idx > total { - let spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; - let frame_idx = (state.misc.tick_count as usize / 2) % spinner_frames.len(); - let spinner = spinner_frames[frame_idx]; - visible.push(Line::from(vec![ - Span::styled( - format!("{} ", format_role_label(&Role::Assistant)), - Style::default() - .fg(Theme::ROLE_ASSISTANT) - .add_modifier(Modifier::BOLD), - ), - Span::styled(format!("{spinner} "), Style::default().fg(Theme::TEXT_DIM)), - Span::styled( - "generating...", + // Spinner hanya ditambahkan jika visible window mencakup posisi terakhir + if state.turn_in_flight() && end_idx > total { + let spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + let frame_idx = (state.misc.tick_count as usize / 2) % spinner_frames.len(); + let spinner = spinner_frames[frame_idx]; + visible.push(Line::from(vec![ + Span::styled( + format!("{} ", format_role_label(&Role::Assistant)), + Style::default() + .fg(Theme::ROLE_ASSISTANT) + .add_modifier(Modifier::BOLD), + ), + Span::styled(format!("{spinner} "), Style::default().fg(Theme::TEXT_DIM)), + Span::styled( + "generating...", + Style::default() + .fg(Theme::TEXT_MUTED) + .add_modifier(Modifier::ITALIC), + ), + ])); + } + + let scroll_pct = if total_with_spinner > max_visible && max_offset > 0 { + ((offset as f64 / max_offset as f64) * 100.0) as u8 + } else { + 0 + }; + + let title = if scroll_pct > 0 { + format!(" 💬 Chat [{} msgs] ── {}% ↑ ", messages.len(), scroll_pct) + } else if messages.is_empty() { + String::from(" 💬 Chat ") + } else { + format!(" 💬 Chat [{} msgs] ", messages.len()) + }; + + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(Theme::BORDER)) + .title(Span::styled( + title, Style::default() .fg(Theme::TEXT_MUTED) - .add_modifier(Modifier::ITALIC), - ), - ])); - } + .add_modifier(Modifier::BOLD), + )); - let scroll_pct = if total_with_spinner > max_visible && max_offset > 0 { - ((offset as f64 / max_offset as f64) * 100.0) as u8 - } else { - 0 - }; + let paragraph = Paragraph::new(visible) + .block(block) + .style(Style::default().bg(Theme::BG)); - let title = if scroll_pct > 0 { - format!(" 💬 Chat [{} msgs] ── {}% ↑ ", messages.len(), scroll_pct) - } else if messages.is_empty() { - String::from(" 💬 Chat ") - } else { - format!(" 💬 Chat [{} msgs] ", messages.len()) - }; - - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(Theme::BORDER)) - .title(Span::styled( - title, - Style::default() - .fg(Theme::TEXT_MUTED) - .add_modifier(Modifier::BOLD), - )); - - let paragraph = Paragraph::new(visible) - .block(block) - .style(Style::default().bg(Theme::BG)); - - frame.render_widget(paragraph, area); + frame.render_widget(paragraph, area); } } diff --git a/apps/interfaces/tui/src/view/input.rs b/apps/interfaces/tui/src/view/input.rs index 1ebb803..9e338a9 100644 --- a/apps/interfaces/tui/src/view/input.rs +++ b/apps/interfaces/tui/src/view/input.rs @@ -33,7 +33,9 @@ impl Component for InputComponent { .border_style(Style::default().fg(Theme::BORDER)) .title(Span::styled( dropdown_title, - Style::default().fg(Theme::PRIMARY).add_modifier(Modifier::BOLD), + Style::default() + .fg(Theme::PRIMARY) + .add_modifier(Modifier::BOLD), )) .style(Style::default().bg(Theme::SURFACE_ELEVATED)); @@ -98,7 +100,7 @@ impl Component for InputComponent { let char_len = c.len_utf8(); (c.to_string(), &after[char_len..]) }; - + // Blinking cursor logic based on tick count let cursor_style = if state.misc.tick_count % 10 < 5 { Style::default() @@ -111,7 +113,7 @@ impl Component for InputComponent { .fg(Theme::BG) .add_modifier(Modifier::BOLD) }; - + spans.push(Span::styled(cursor_char, cursor_style)); if !after_char.is_empty() { spans.push(Span::raw(after_char.to_string())); diff --git a/apps/interfaces/tui/src/view/markdown.rs b/apps/interfaces/tui/src/view/markdown.rs index f93cefa..ceacdce 100644 --- a/apps/interfaces/tui/src/view/markdown.rs +++ b/apps/interfaces/tui/src/view/markdown.rs @@ -60,200 +60,195 @@ pub fn render_markdown(text: &str, width: u16, dim: bool) -> Vec> for event in parser { match event { - pulldown_cmark::Event::Start(tag) => { - match tag { - pulldown_cmark::Tag::CodeBlock(kind) => { - in_code_block = true; - in_diff_block = matches!( - &kind, - pulldown_cmark::CodeBlockKind::Fenced(lang) if lang.as_ref() == "diff" - ); - spans.push(Span::styled("\n", Style::default())); - spans.push(Span::styled( - " ┌─ code ", - apply_dim( - Style::default().fg(Theme::TEXT_MUTED).bg(Theme::CODE_BG), - dim, - ), - )); - spans.push(Span::styled("\n", Style::default())); - } - pulldown_cmark::Tag::Heading { level, .. } => { - in_heading = true; - heading_level = match level { - pulldown_cmark::HeadingLevel::H1 => 1, - pulldown_cmark::HeadingLevel::H2 => 2, - pulldown_cmark::HeadingLevel::H3 => 3, - _ => 4, - }; - } - pulldown_cmark::Tag::Item => { - spans.push(Span::styled( - "• ", - apply_dim(Style::default().fg(Theme::PRIMARY), dim), - )); - } - pulldown_cmark::Tag::Link { dest_url, .. } => { - spans.push(Span::styled( - "[", - apply_dim(Style::default().fg(Theme::INFO), dim), - )); - spans.push(Span::styled( - format!("]({dest_url})"), - apply_dim( - Style::default() - .fg(Theme::TEXT_MUTED) - .add_modifier(Modifier::ITALIC), - dim, - ), - )); - } - pulldown_cmark::Tag::BlockQuote(_) => { - spans.push(Span::styled( - "▎", - apply_dim(Style::default().fg(Theme::BLOCKQUOTE_BAR), dim), - )); - } - pulldown_cmark::Tag::Table(_) => { - table_rows.clear(); - } - pulldown_cmark::Tag::TableHead | pulldown_cmark::Tag::TableRow => { - current_row.clear(); - } - pulldown_cmark::Tag::TableCell => { - in_table_cell = true; - current_cell.clear(); - } - _ => {} + pulldown_cmark::Event::Start(tag) => match tag { + pulldown_cmark::Tag::CodeBlock(kind) => { + in_code_block = true; + in_diff_block = matches!( + &kind, + pulldown_cmark::CodeBlockKind::Fenced(lang) if lang.as_ref() == "diff" + ); + spans.push(Span::styled("\n", Style::default())); + spans.push(Span::styled( + " ┌─ code ", + apply_dim( + Style::default().fg(Theme::TEXT_MUTED).bg(Theme::CODE_BG), + dim, + ), + )); + spans.push(Span::styled("\n", Style::default())); } - } - pulldown_cmark::Event::End(tag) => { - match tag { - pulldown_cmark::TagEnd::CodeBlock => { - in_code_block = false; - in_diff_block = false; - spans.push(Span::styled( - "\n └─\n", - apply_dim( - Style::default().fg(Theme::TEXT_MUTED).bg(Theme::CODE_BG), - dim, - ), - )); + pulldown_cmark::Tag::Heading { level, .. } => { + in_heading = true; + heading_level = match level { + pulldown_cmark::HeadingLevel::H1 => 1, + pulldown_cmark::HeadingLevel::H2 => 2, + pulldown_cmark::HeadingLevel::H3 => 3, + _ => 4, + }; + } + pulldown_cmark::Tag::Item => { + spans.push(Span::styled( + "• ", + apply_dim(Style::default().fg(Theme::PRIMARY), dim), + )); + } + pulldown_cmark::Tag::Link { dest_url, .. } => { + spans.push(Span::styled( + "[", + apply_dim(Style::default().fg(Theme::INFO), dim), + )); + spans.push(Span::styled( + format!("]({dest_url})"), + apply_dim( + Style::default() + .fg(Theme::TEXT_MUTED) + .add_modifier(Modifier::ITALIC), + dim, + ), + )); + } + pulldown_cmark::Tag::BlockQuote(_) => { + spans.push(Span::styled( + "▎", + apply_dim(Style::default().fg(Theme::BLOCKQUOTE_BAR), dim), + )); + } + pulldown_cmark::Tag::Table(_) => { + table_rows.clear(); + } + pulldown_cmark::Tag::TableHead | pulldown_cmark::Tag::TableRow => { + current_row.clear(); + } + pulldown_cmark::Tag::TableCell => { + in_table_cell = true; + current_cell.clear(); + } + _ => {} + }, + pulldown_cmark::Event::End(tag) => match tag { + pulldown_cmark::TagEnd::CodeBlock => { + in_code_block = false; + in_diff_block = false; + spans.push(Span::styled( + "\n └─\n", + apply_dim( + Style::default().fg(Theme::TEXT_MUTED).bg(Theme::CODE_BG), + dim, + ), + )); + } + pulldown_cmark::TagEnd::Heading(_) => { + in_heading = false; + heading_level = 0; + spans.push(Span::raw("\n")); + } + pulldown_cmark::TagEnd::Paragraph => { + spans.push(Span::raw("\n\n")); + } + pulldown_cmark::TagEnd::Item | pulldown_cmark::TagEnd::BlockQuote(_) => { + spans.push(Span::raw("\n")); + } + pulldown_cmark::TagEnd::TableCell => { + in_table_cell = false; + current_row.push(std::mem::take(&mut current_cell)); + } + pulldown_cmark::TagEnd::TableHead | pulldown_cmark::TagEnd::TableRow => { + table_rows.push(std::mem::take(&mut current_row)); + } + pulldown_cmark::TagEnd::Table => { + let cols_count = table_rows.first().map_or(0, std::vec::Vec::len); + if cols_count == 0 { + continue; } - pulldown_cmark::TagEnd::Heading(_) => { - in_heading = false; - heading_level = 0; - spans.push(Span::raw("\n")); - } - pulldown_cmark::TagEnd::Paragraph => { - spans.push(Span::raw("\n\n")); - } - pulldown_cmark::TagEnd::Item | pulldown_cmark::TagEnd::BlockQuote(_) => { - spans.push(Span::raw("\n")); - } - pulldown_cmark::TagEnd::TableCell => { - in_table_cell = false; - current_row.push(std::mem::take(&mut current_cell)); - } - pulldown_cmark::TagEnd::TableHead | pulldown_cmark::TagEnd::TableRow => { - table_rows.push(std::mem::take(&mut current_row)); - } - pulldown_cmark::TagEnd::Table => { - let cols_count = table_rows.first().map_or(0, std::vec::Vec::len); - if cols_count == 0 { - continue; - } - let mut col_widths = vec![0; cols_count]; - for row in &table_rows { - for (i, cell) in row.iter().enumerate() { - if i < cols_count { - let cell_width: usize = - cell.iter().map(|s| s.content.chars().count()).sum(); - if cell_width > col_widths[i] { - col_widths[i] = cell_width; - } + let mut col_widths = vec![0; cols_count]; + for row in &table_rows { + for (i, cell) in row.iter().enumerate() { + if i < cols_count { + let cell_width: usize = + cell.iter().map(|s| s.content.chars().count()).sum(); + if cell_width > col_widths[i] { + col_widths[i] = cell_width; } } } - let effective_width = if width > 0 { - (width as usize).saturating_sub(2) - } else { - 0 - }; - let border_overhead = cols_count * 3 + 4; - let available_width = effective_width.saturating_sub(border_overhead); - let mut total_width: usize = col_widths.iter().sum(); + } + let effective_width = if width > 0 { + (width as usize).saturating_sub(2) + } else { + 0 + }; + let border_overhead = cols_count * 3 + 4; + let available_width = effective_width.saturating_sub(border_overhead); + let mut total_width: usize = col_widths.iter().sum(); - if width > 0 && total_width > available_width && available_width > 0 { - while total_width > available_width { - let Some(max_idx) = col_widths - .iter() - .enumerate() - .max_by_key(|&(_, &w)| w) - .map(|(i, _)| i) - else { - break; - }; - if col_widths[max_idx] <= 3 { - break; - } - col_widths[max_idx] -= 1; - total_width -= 1; + if width > 0 && total_width > available_width && available_width > 0 { + while total_width > available_width { + let Some(max_idx) = col_widths + .iter() + .enumerate() + .max_by_key(|&(_, &w)| w) + .map(|(i, _)| i) + else { + break; + }; + if col_widths[max_idx] <= 3 { + break; + } + col_widths[max_idx] -= 1; + total_width -= 1; + } + } + + spans.push(Span::raw("\n")); + for (r, row) in table_rows.iter().enumerate() { + let mut cell_lines = Vec::new(); + for (i, cell) in row.iter().enumerate() { + if i < cols_count { + cell_lines.push(wrap_spans_to_lines(cell, col_widths[i])); } } + let max_height = + cell_lines.iter().map(std::vec::Vec::len).max().unwrap_or(1); - spans.push(Span::raw("\n")); - for (r, row) in table_rows.iter().enumerate() { - let mut cell_lines = Vec::new(); - for (i, cell) in row.iter().enumerate() { - if i < cols_count { - cell_lines.push(wrap_spans_to_lines(cell, col_widths[i])); + for y in 0..max_height { + spans.push(Span::styled( + " | ", + apply_dim(Style::default().fg(Theme::BORDER), dim), + )); + for (i, cl) in cell_lines.iter().enumerate() { + let line_spans = if y < cl.len() { &cl[y] } else { [].as_slice() }; + let mut line_width = 0; + for span in line_spans { + line_width += span.content.chars().count(); + spans.push(span.clone()); } - } - let max_height = - cell_lines.iter().map(std::vec::Vec::len).max().unwrap_or(1); - - for y in 0..max_height { + let pad = col_widths[i].saturating_sub(line_width); + spans.push(Span::raw(" ".repeat(pad))); spans.push(Span::styled( - " | ", + " | ", apply_dim(Style::default().fg(Theme::BORDER), dim), )); - for (i, cl) in cell_lines.iter().enumerate() { - let line_spans = - if y < cl.len() { &cl[y] } else { [].as_slice() }; - let mut line_width = 0; - for span in line_spans { - line_width += span.content.chars().count(); - spans.push(span.clone()); - } - let pad = col_widths[i].saturating_sub(line_width); - spans.push(Span::raw(" ".repeat(pad))); - spans.push(Span::styled( - " | ", - apply_dim(Style::default().fg(Theme::BORDER), dim), - )); - } - spans.push(Span::raw("\n")); } - if r == 0 { + spans.push(Span::raw("\n")); + } + if r == 0 { + spans.push(Span::styled( + " | ", + apply_dim(Style::default().fg(Theme::BORDER), dim), + )); + for w in &col_widths { spans.push(Span::styled( - " | ", + format!("{}| ", "-".repeat(*w + 1)), apply_dim(Style::default().fg(Theme::BORDER), dim), )); - for w in &col_widths { - spans.push(Span::styled( - format!("{}| ", "-".repeat(*w + 1)), - apply_dim(Style::default().fg(Theme::BORDER), dim), - )); - } - spans.push(Span::raw("\n")); } + spans.push(Span::raw("\n")); } - spans.push(Span::raw("\n")); } - _ => {} + spans.push(Span::raw("\n")); } - } + _ => {} + }, pulldown_cmark::Event::Text(text) => { let s = text.to_string(); if in_code_block { diff --git a/apps/interfaces/tui/src/view/overlays/bash.rs b/apps/interfaces/tui/src/view/overlays/bash.rs index d176479..6fcfb7d 100644 --- a/apps/interfaces/tui/src/view/overlays/bash.rs +++ b/apps/interfaces/tui/src/view/overlays/bash.rs @@ -1,9 +1,9 @@ //! Overlay: list of active / completed bash background jobs. +use crate::view::theme::Theme; use ratatui::style::Style; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the Bash Jobs overlay. diff --git a/apps/interfaces/tui/src/view/overlays/clear_confirm.rs b/apps/interfaces/tui/src/view/overlays/clear_confirm.rs index 7229453..01168fe 100644 --- a/apps/interfaces/tui/src/view/overlays/clear_confirm.rs +++ b/apps/interfaces/tui/src/view/overlays/clear_confirm.rs @@ -1,9 +1,9 @@ //! Overlay: confirm-before-clear dialog for the chat transcript. +use crate::view::theme::Theme; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the Clear Transcript confirmation dialog. diff --git a/apps/interfaces/tui/src/view/overlays/diff.rs b/apps/interfaces/tui/src/view/overlays/diff.rs index b43c1a2..5d199a6 100644 --- a/apps/interfaces/tui/src/view/overlays/diff.rs +++ b/apps/interfaces/tui/src/view/overlays/diff.rs @@ -5,14 +5,14 @@ //! coloring (green `+`, red `-`, blue header, dim context) → render in a //! scrollable paragraph widget. +use super::super::theme::Theme; +use super::overlay_block; use crate::state::AppStateRest; use ratatui::layout::Rect; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph, Wrap}; use ratatui::Frame; -use super::super::theme::Theme; -use super::overlay_block; /// Render the diff preview overlay with color-coded +/- lines and scrolling. /// @@ -54,9 +54,7 @@ pub fn render(frame: &mut Frame, area: Rect, block: Block<'static>, state: &AppS // Hunk header — cyan Line::from(Span::styled( format!("{trimmed}\n"), - Style::default() - .fg(Theme::INFO) - .add_modifier(Modifier::DIM), + Style::default().fg(Theme::INFO).add_modifier(Modifier::DIM), )) } else if trimmed.starts_with("diff --git") || trimmed.starts_with("index ") diff --git a/apps/interfaces/tui/src/view/overlays/editor.rs b/apps/interfaces/tui/src/view/overlays/editor.rs index 6a48e76..280940e 100644 --- a/apps/interfaces/tui/src/view/overlays/editor.rs +++ b/apps/interfaces/tui/src/view/overlays/editor.rs @@ -1,10 +1,10 @@ //! Overlay: inline editor mode — shows the current input buffer with cursor //! position and save/dismiss key hints. +use crate::view::theme::Theme; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the Editor overlay. @@ -15,7 +15,10 @@ pub fn render( block: Block<'static>, state: &crate::state::AppStateRest, ) { - debug!("Rendering Editor overlay, buffer length: {}", state.input.buffer.len()); + debug!( + "Rendering Editor overlay, buffer length: {}", + state.input.buffer.len() + ); let block = block .title(Span::styled( " Editor ", diff --git a/apps/interfaces/tui/src/view/overlays/help.rs b/apps/interfaces/tui/src/view/overlays/help.rs index 5e65049..cd6bb60 100644 --- a/apps/interfaces/tui/src/view/overlays/help.rs +++ b/apps/interfaces/tui/src/view/overlays/help.rs @@ -1,8 +1,8 @@ //! Overlay: keyboard shortcut reference. +use crate::view::theme::Theme; use ratatui::style::Style; use ratatui::widgets::{Block, Paragraph, Wrap}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the Help overlay. @@ -13,7 +13,10 @@ pub fn render( block: Block<'static>, state: &crate::state::AppStateRest, ) { - debug!("Rendering Help overlay, content length: {}", state.help_text.len()); + debug!( + "Rendering Help overlay, content length: {}", + state.help_text.len() + ); let block = super::overlay_block(block, "Help", Theme::INFO); let content = state.help_text; let paragraph = Paragraph::new(content) diff --git a/apps/interfaces/tui/src/view/overlays/key_input.rs b/apps/interfaces/tui/src/view/overlays/key_input.rs index 746c374..f4e98fa 100644 --- a/apps/interfaces/tui/src/view/overlays/key_input.rs +++ b/apps/interfaces/tui/src/view/overlays/key_input.rs @@ -1,10 +1,10 @@ //! Overlay: API key input dialog — prompts the user for a provider API key //! with masked display (shows first 4 chars only). +use crate::view::theme::Theme; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the API Key input overlay. diff --git a/apps/interfaces/tui/src/view/overlays/learning.rs b/apps/interfaces/tui/src/view/overlays/learning.rs index 8e9f578..6c79f10 100644 --- a/apps/interfaces/tui/src/view/overlays/learning.rs +++ b/apps/interfaces/tui/src/view/overlays/learning.rs @@ -71,8 +71,14 @@ pub fn render( Style::default().fg(Theme::WARNING) }, ), - LearningItem::Stored { name, lifecycle, .. } => { - let status = if lifecycle == "stale" { "Stale" } else { "Active" }; + LearningItem::Stored { + name, lifecycle, .. + } => { + let status = if lifecycle == "stale" { + "Stale" + } else { + "Active" + }; ( format!("{prefix}[{status}] {name}"), if is_selected { @@ -110,7 +116,12 @@ pub fn render( let mut right_lines = Vec::new(); if let Some(item) = items.get(selected) { match item { - LearningItem::Pending { name, content, scope, confidence } => { + LearningItem::Pending { + name, + content, + scope, + confidence, + } => { right_lines.push(Line::from(Span::styled( " Name:", Style::default().fg(Theme::TEXT_DIM), @@ -151,7 +162,13 @@ pub fn render( Style::default().fg(Theme::TEXT_DIM), ))); } - LearningItem::Stored { name, content, lifecycle, scope, description } => { + LearningItem::Stored { + name, + content, + lifecycle, + scope, + description, + } => { right_lines.push(Line::from(Span::styled( " Name:", Style::default().fg(Theme::TEXT_DIM), @@ -163,7 +180,11 @@ pub fn render( .add_modifier(Modifier::BOLD), ))); right_lines.push(Line::from(Span::raw(""))); - let status_color = if lifecycle == "stale" { Theme::WARNING } else { Theme::SUCCESS }; + let status_color = if lifecycle == "stale" { + Theme::WARNING + } else { + Theme::SUCCESS + }; right_lines.push(Line::from(Span::styled( format!(" Status: {lifecycle}"), Style::default().fg(status_color), diff --git a/apps/interfaces/tui/src/view/overlays/loading.rs b/apps/interfaces/tui/src/view/overlays/loading.rs index e7f0dee..dd925c4 100644 --- a/apps/interfaces/tui/src/view/overlays/loading.rs +++ b/apps/interfaces/tui/src/view/overlays/loading.rs @@ -1,9 +1,9 @@ //! Overlay: loading / processing spinner — shown during blocking operations. +use crate::view::theme::Theme; use ratatui::style::{Modifier, Style}; use ratatui::text::Span; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the Loading overlay. diff --git a/apps/interfaces/tui/src/view/overlays/mcp.rs b/apps/interfaces/tui/src/view/overlays/mcp.rs index e826abf..e87e755 100644 --- a/apps/interfaces/tui/src/view/overlays/mcp.rs +++ b/apps/interfaces/tui/src/view/overlays/mcp.rs @@ -1,9 +1,9 @@ //! Overlay: MCP (Model Context Protocol) server management. +use crate::view::theme::Theme; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the MCP Servers overlay. @@ -14,7 +14,10 @@ pub fn render( block: Block<'static>, state: &crate::state::AppStateRest, ) { - debug!("Rendering MCP Servers overlay, session dir: {}", state.session_dir.display()); + debug!( + "Rendering MCP Servers overlay, session dir: {}", + state.session_dir.display() + ); let block = block .title(Span::styled( " MCP Servers ", diff --git a/apps/interfaces/tui/src/view/overlays/mod.rs b/apps/interfaces/tui/src/view/overlays/mod.rs index ccd3c1a..960a82f 100644 --- a/apps/interfaces/tui/src/view/overlays/mod.rs +++ b/apps/interfaces/tui/src/view/overlays/mod.rs @@ -13,30 +13,32 @@ pub mod learning; pub mod loading; pub mod mcp; pub mod model_selector; +pub mod plan; pub mod quit_confirm; pub mod rewind; pub mod settings; pub mod todo; -pub mod plan; pub mod usage; +use super::theme::Theme; use crate::state::{AppStateRest, Overlay}; use ratatui::layout::Rect; use ratatui::style::{Modifier, Style}; use ratatui::text::Span; use ratatui::widgets::{Block, Borders, Clear}; use ratatui::Frame; -use super::theme::Theme; use tracing::debug; /// Decorate an overlay block with a styled title and matching border color. -pub fn overlay_block(block: Block<'static>, title: &str, color: ratatui::style::Color) -> Block<'static> { +pub fn overlay_block( + block: Block<'static>, + title: &str, + color: ratatui::style::Color, +) -> Block<'static> { block .title(Span::styled( format!(" {title} "), - Style::default() - .fg(color) - .add_modifier(Modifier::BOLD), + Style::default().fg(color).add_modifier(Modifier::BOLD), )) .border_style(Style::default().fg(color)) } @@ -59,12 +61,7 @@ pub fn centered_rect(area: Rect, percent_x: u16, percent_y: u16) -> Rect { /// Dispatches to the appropriate overlay module's `render` function based on the /// `Overlay` variant. No-ops for `Overlay::None`. #[tracing::instrument(skip(frame, state))] -pub fn render_overlay( - frame: &mut Frame, - area: Rect, - overlay: Overlay, - state: &AppStateRest, -) { +pub fn render_overlay(frame: &mut Frame, area: Rect, overlay: Overlay, state: &AppStateRest) { debug!("Rendering overlay: {overlay:?}"); let overlay_area = centered_rect(area, 75, 70); frame.render_widget(Clear, overlay_area); diff --git a/apps/interfaces/tui/src/view/overlays/model_selector.rs b/apps/interfaces/tui/src/view/overlays/model_selector.rs index a5473a3..4ea7b13 100644 --- a/apps/interfaces/tui/src/view/overlays/model_selector.rs +++ b/apps/interfaces/tui/src/view/overlays/model_selector.rs @@ -15,7 +15,10 @@ pub fn render( block: Block<'static>, state: &crate::state::AppStateRest, ) { - debug!("Rendering Model Selector overlay, current provider: {}, model: {}", state.settings.provider, state.settings.model); + debug!( + "Rendering Model Selector overlay, current provider: {}, model: {}", + state.settings.provider, state.settings.model + ); let block = block .title(Span::styled( " Model Selector ", diff --git a/apps/interfaces/tui/src/view/overlays/plan.rs b/apps/interfaces/tui/src/view/overlays/plan.rs index 4f83e33..845ce54 100644 --- a/apps/interfaces/tui/src/view/overlays/plan.rs +++ b/apps/interfaces/tui/src/view/overlays/plan.rs @@ -1,9 +1,9 @@ //! Overlay: Project Plan view — shows the full project plan content. +use crate::view::theme::Theme; use ratatui::style::{Modifier, Style}; use ratatui::text::Span; use ratatui::widgets::{Block, Paragraph, Wrap}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the Project Plan overlay. diff --git a/apps/interfaces/tui/src/view/overlays/quit_confirm.rs b/apps/interfaces/tui/src/view/overlays/quit_confirm.rs index d51edca..65371c0 100644 --- a/apps/interfaces/tui/src/view/overlays/quit_confirm.rs +++ b/apps/interfaces/tui/src/view/overlays/quit_confirm.rs @@ -1,9 +1,9 @@ //! Overlay: quit confirmation dialog. +use crate::view::theme::Theme; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the Quit confirmation overlay. diff --git a/apps/interfaces/tui/src/view/overlays/rewind.rs b/apps/interfaces/tui/src/view/overlays/rewind.rs index 14e065d..4be6f38 100644 --- a/apps/interfaces/tui/src/view/overlays/rewind.rs +++ b/apps/interfaces/tui/src/view/overlays/rewind.rs @@ -40,7 +40,11 @@ pub fn render( Style::default().fg(Theme::TEXT_DIM), ))); } else { - let start = if messages.len() > 8 { messages.len() - 8 } else { 0 }; + let start = if messages.len() > 8 { + messages.len() - 8 + } else { + 0 + }; for msg in messages.iter().skip(start) { let role_str = match msg.role { Role::User => "User", @@ -51,7 +55,11 @@ pub fn render( let preview: String = msg.content.chars().take(70).collect(); lines.push(Line::from(Span::styled( format!(" [{role_str}] {preview}"), - Style::default().fg(if msg.role == Role::User { Theme::INFO } else { Theme::TEXT }), + Style::default().fg(if msg.role == Role::User { + Theme::INFO + } else { + Theme::TEXT + }), ))); } if messages.len() > 8 { diff --git a/apps/interfaces/tui/src/view/overlays/settings.rs b/apps/interfaces/tui/src/view/overlays/settings.rs index 97aef29..b699081 100644 --- a/apps/interfaces/tui/src/view/overlays/settings.rs +++ b/apps/interfaces/tui/src/view/overlays/settings.rs @@ -1,10 +1,10 @@ //! Overlay: settings overview — displays the current provider, model, //! max tokens, temperature, internet mode, and review toggle. +use crate::view::theme::Theme; use ratatui::style::Style; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the Settings overlay. @@ -15,7 +15,10 @@ pub fn render( block: Block<'static>, state: &crate::state::AppStateRest, ) { - debug!("Rendering Settings overlay, provider: {}, model: {}", state.settings.provider, state.settings.model); + debug!( + "Rendering Settings overlay, provider: {}, model: {}", + state.settings.provider, state.settings.model + ); let block = super::overlay_block(block, "Settings", Theme::PRIMARY); let lines = vec![ Line::from(Span::styled( @@ -29,14 +32,20 @@ pub fn render( Line::from(Span::styled( format!( " Max tokens: {}", - state.settings.max_tokens.map_or_else(|| "auto".to_string(), |v| v.to_string()) + state + .settings + .max_tokens + .map_or_else(|| "auto".to_string(), |v| v.to_string()) ), Style::default().fg(Theme::TEXT), )), Line::from(Span::styled( format!( " Temperature: {}", - state.settings.temperature.map_or_else(|| "auto".to_string(), |v| format!("{v:.1}")) + state + .settings + .temperature + .map_or_else(|| "auto".to_string(), |v| format!("{v:.1}")) ), Style::default().fg(Theme::TEXT), )), diff --git a/apps/interfaces/tui/src/view/overlays/todo.rs b/apps/interfaces/tui/src/view/overlays/todo.rs index 2a12a68..0b13cfb 100644 --- a/apps/interfaces/tui/src/view/overlays/todo.rs +++ b/apps/interfaces/tui/src/view/overlays/todo.rs @@ -1,9 +1,9 @@ //! Overlay: Tasks (todo) view — shows the full todo list content. +use crate::view::theme::Theme; use ratatui::style::{Modifier, Style}; use ratatui::text::Span; use ratatui::widgets::{Block, Paragraph, Wrap}; use ratatui::Frame; -use crate::view::theme::Theme; use tracing::debug; /// Render the Tasks / Todo overlay. diff --git a/apps/interfaces/tui/src/view/overlays/usage.rs b/apps/interfaces/tui/src/view/overlays/usage.rs index aa47b76..32fa995 100644 --- a/apps/interfaces/tui/src/view/overlays/usage.rs +++ b/apps/interfaces/tui/src/view/overlays/usage.rs @@ -31,7 +31,12 @@ pub fn render( let summary = runtime.map(|r| compute_usage_summary(&r.usage, r.session_start, now_ms)); let (edit_count, lesson_count, review_count, consec_empty) = runtime.map_or((0, 0, 0, 0), |r| { - (r.edit_count, r.lessons.total, r.review_count, r.consecutive_empty_reviews) + ( + r.edit_count, + r.lessons.total, + r.review_count, + r.consecutive_empty_reviews, + ) }); let mut lines = vec![ Line::from(Span::styled( @@ -88,12 +93,19 @@ pub fn render( ))); lines.push(Line::from(Span::styled( format!(" Empty reviews: {consec_empty}"), - Style::default().fg(if consec_empty > 3 { Theme::WARNING } else { Theme::TEXT_DIM }), + Style::default().fg(if consec_empty > 3 { + Theme::WARNING + } else { + Theme::TEXT_DIM + }), ))); if let Some(s) = &summary { lines.push(Line::from(Span::raw(""))); lines.push(Line::from(Span::styled( - format!(" Session: {}h {}m {}s", s.elapsed_hours, s.elapsed_minutes, s.elapsed_seconds), + format!( + " Session: {}h {}m {}s", + s.elapsed_hours, s.elapsed_minutes, s.elapsed_seconds + ), Style::default().fg(Theme::TEXT_DIM), ))); } diff --git a/apps/interfaces/tui/src/view/sidebar.rs b/apps/interfaces/tui/src/view/sidebar.rs index 1e665ad..337926f 100644 --- a/apps/interfaces/tui/src/view/sidebar.rs +++ b/apps/interfaces/tui/src/view/sidebar.rs @@ -52,7 +52,7 @@ fn draw_tasks_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppSta .border_type(BorderType::Rounded) .border_style(Style::default().fg(Theme::BORDER)) .style(Style::default().bg(Theme::SURFACE)); - + let inner_area = block.inner(area); let budget = (inner_area.height as usize).max(1); @@ -62,7 +62,9 @@ fn draw_tasks_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppSta let lines: Vec = if task_lines.is_empty() { vec![Line::from(Span::styled( " No tasks yet.", - Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC), + Style::default() + .fg(Theme::TEXT_DIM) + .add_modifier(Modifier::ITALIC), ))] } else { let show_hint = task_lines.len() > budget; @@ -113,15 +115,13 @@ fn draw_usage_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppSta } else { state.cached_token_count }; - - let mut items = vec![ - Line::from(Span::styled( - format!(" {:<6}: {} tok", "Total", summary.total_tokens), - Style::default() - .fg(Theme::TEXT) - .add_modifier(Modifier::BOLD), - )), - ]; + + let mut items = vec![Line::from(Span::styled( + format!(" {:<6}: {} tok", "Total", summary.total_tokens), + Style::default() + .fg(Theme::TEXT) + .add_modifier(Modifier::BOLD), + ))]; if summary.self_learning_tokens > 0 { items.push(Line::from(Span::styled( @@ -159,12 +159,14 @@ fn draw_usage_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppSta Style::default().fg(Theme::TEXT_DIM), )), ]); - + items } else { vec![Line::from(Span::styled( " No active session.", - Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC), + Style::default() + .fg(Theme::TEXT_DIM) + .add_modifier(Modifier::ITALIC), ))] }; diff --git a/apps/interfaces/tui/src/view/status.rs b/apps/interfaces/tui/src/view/status.rs index c13a733..2739f1c 100644 --- a/apps/interfaces/tui/src/view/status.rs +++ b/apps/interfaces/tui/src/view/status.rs @@ -44,7 +44,10 @@ impl Component for StatusBarComponent { status_badge, ]; - let right_str = format!(" Provider: {} | Model: {} ", state.settings.provider, state.settings.model); + let right_str = format!( + " Provider: {} | Model: {} ", + state.settings.provider, state.settings.model + ); let left_line = Line::from(left_spans); let right_line = Line::from(Span::styled( @@ -72,7 +75,8 @@ impl Component for StatusBarComponent { ]) .split(area); - let block = Block::default().style(Style::default().bg(Theme::STATUS_BAR_BG).fg(Theme::TEXT)); + let block = + Block::default().style(Style::default().bg(Theme::STATUS_BAR_BG).fg(Theme::TEXT)); let left_para = ratatui::widgets::Paragraph::new(left_line).block(block.clone()); frame.render_widget(left_para, chunks[0]); diff --git a/apps/interfaces/tui/src/view/workflow.rs b/apps/interfaces/tui/src/view/workflow.rs index 003df86..0848770 100644 --- a/apps/interfaces/tui/src/view/workflow.rs +++ b/apps/interfaces/tui/src/view/workflow.rs @@ -41,11 +41,7 @@ fn state_color(state: AgentState) -> Color { /// and body (agent cards with icon/name/label/duration, or session stats /// when no workflow is running). #[instrument(skip_all)] -pub fn draw_workflow_panel( - frame: &mut Frame, - area: Rect, - state: &crate::state::AppStateRest, -) { +pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest) { debug!("draw_workflow_panel — rendering workflow panel"); use ratatui::layout::{Constraint, Direction, Layout}; diff --git a/apps/interfaces/web/src/lib.rs b/apps/interfaces/web/src/lib.rs index 67eb0a2..fbc0b80 100644 --- a/apps/interfaces/web/src/lib.rs +++ b/apps/interfaces/web/src/lib.rs @@ -55,7 +55,8 @@ h1{color:#7aa2f7}a{color:#bb9af7}

  • Build the frontend: cd apps/interfaces/web && npm install && npm run build
  • Restart with --web-dir apps/interfaces/web/dist
  • -"#.to_string(), +"# + .to_string(), )) } } diff --git a/apps/interfaces/ws/src/lib.rs b/apps/interfaces/ws/src/lib.rs index 47bec4d..46a2c27 100644 --- a/apps/interfaces/ws/src/lib.rs +++ b/apps/interfaces/ws/src/lib.rs @@ -47,12 +47,18 @@ async fn ws_handler( Query(query): Query, axum::extract::State(state): axum::extract::State>, ) -> impl IntoResponse { - let configured = std::env::var("ZESDEX_WS_TOKEN").ok().filter(|s| !s.is_empty()); + let configured = std::env::var("ZESDEX_WS_TOKEN") + .ok() + .filter(|s| !s.is_empty()); match configured { Some(expected) if query.token.as_deref() != Some(expected.as_str()) => { warn!("rejecting WS connection: missing/invalid token"); // 401 Unauthorized — client did not present the required token. - (axum::http::StatusCode::UNAUTHORIZED, "missing or invalid token").into_response() + ( + axum::http::StatusCode::UNAUTHORIZED, + "missing or invalid token", + ) + .into_response() } _ => ws.on_upgrade(move |socket| handle_socket(socket, state)), } @@ -95,17 +101,22 @@ async fn handle_socket(mut socket: WebSocket, state: Arc) { Message::Text(text) => { let text_str = text.to_string(); info!("Received WS message: {text_str}"); - + if let Ok(val) = serde_json::from_str::(&text_str) { if val.get("type").and_then(|v| v.as_str()) == Some("prompt") { if let Some(prompt) = val.get("message").and_then(|v| v.as_str()) { let session_dir = std::env::current_dir().unwrap_or_default(); let workspace_roots = vec![session_dir.clone()]; - let turn_events = Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())); + let turn_events = + Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())); let in_flight = Arc::new(std::sync::atomic::AtomicBool::new(false)); let abort = Arc::new(std::sync::atomic::AtomicBool::new(false)); let api_key = std::env::var("OPENAI_API_KEY").unwrap_or_default(); - let model = val.get("model").and_then(|v| v.as_str()).unwrap_or("gpt-4o").to_string(); + let model = val + .get("model") + .and_then(|v| v.as_str()) + .unwrap_or("gpt-4o") + .to_string(); let params = zesdex_domain::agent::AgentTurnParams { messages: vec![zesdex_domain::core::ChatMessage::user(prompt)], @@ -119,11 +130,11 @@ async fn handle_socket(mut socket: WebSocket, state: Arc) { api_base: None, }; - let client = std::sync::Arc::new(zesdex_infrastructure::llm::provider::LlmClient::new( - api_key, - model, - None, - )); + let client = std::sync::Arc::new( + zesdex_infrastructure::llm::provider::LlmClient::new( + api_key, model, None, + ), + ); let tool_ctx = zesdex_infrastructure::tools::ToolCtx::builder() .session_dir(session_dir) @@ -138,11 +149,12 @@ async fn handle_socket(mut socket: WebSocket, state: Arc) { let tools = zesdex_infrastructure::tools::all_tools(); let defs = zesdex_infrastructure::tools::tool_defs(&tools); - let turn_service = zesdex_application::agent::turn_service::AgentTurnServiceImpl::new( - client, - tool_executor, - defs, - ); + let turn_service = + zesdex_application::agent::turn_service::AgentTurnServiceImpl::new( + client, + tool_executor, + defs, + ); use zesdex_application::agent::AgentTurnService; tokio::spawn(async move {