refactor(core): remove provider module, migrate to LLMProvider, add client mode handling, improve serialization error handling, update workspace edition, and clean up conditionals and imports

This commit is contained in:
2025-10-12 12:38:55 +02:00
parent c2f5ccea3b
commit 7851af14a9
63 changed files with 2221 additions and 1236 deletions

View File

@@ -5,25 +5,27 @@ use crate::credentials::CredentialManager;
use crate::encryption::{self, VaultHandle};
use crate::formatting::MessageFormatter;
use crate::input::InputBuffer;
use crate::mcp::McpToolCall;
use crate::mcp::client::McpClient;
use crate::mcp::factory::McpClientFactory;
use crate::mcp::permission::PermissionLayer;
use crate::mcp::McpToolCall;
use crate::mode::Mode;
use crate::model::{DetailedModelInfo, ModelManager};
use crate::provider::{ChatStream, Provider};
use crate::providers::OllamaProvider;
use crate::storage::{SessionMeta, StorageManager};
use crate::types::{
ChatParameters, ChatRequest, ChatResponse, Conversation, Message, ModelInfo, ToolCall,
};
use crate::ui::UiController;
use crate::validation::{get_builtin_schemas, SchemaValidator};
use crate::validation::{SchemaValidator, get_builtin_schemas};
use crate::{ChatStream, Provider};
use crate::{
CodeExecTool, ResourcesDeleteTool, ResourcesGetTool, ResourcesListTool, ResourcesWriteTool,
ToolRegistry, WebScrapeTool, WebSearchDetailedTool, WebSearchTool,
};
use crate::{Error, Result};
use log::warn;
use serde_json::Value;
use std::env;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
@@ -38,6 +40,51 @@ pub enum SessionOutcome {
},
}
fn extract_resource_content(value: &Value) -> Option<String> {
match value {
Value::Null => Some(String::new()),
Value::Bool(flag) => Some(flag.to_string()),
Value::Number(num) => Some(num.to_string()),
Value::String(text) => Some(text.clone()),
Value::Array(items) => {
let mut segments = Vec::new();
for item in items {
if let Some(segment) = extract_resource_content(item)
&& !segment.is_empty()
{
segments.push(segment);
}
}
if segments.is_empty() {
None
} else {
Some(segments.join("\n"))
}
}
Value::Object(map) => {
const PREFERRED_FIELDS: [&str; 6] =
["content", "contents", "text", "value", "body", "data"];
for key in PREFERRED_FIELDS.iter() {
if let Some(inner) = map.get(*key)
&& let Some(text) = extract_resource_content(inner)
&& !text.is_empty()
{
return Some(text);
}
}
if let Some(inner) = map.get("chunks")
&& let Some(text) = extract_resource_content(inner)
&& !text.is_empty()
{
return Some(text);
}
None
}
}
}
pub struct SessionController {
provider: Arc<dyn Provider>,
conversation: ConversationManager,
@@ -55,6 +102,7 @@ pub struct SessionController {
credential_manager: Option<Arc<CredentialManager>>,
ui: Arc<dyn UiController>,
enable_code_tools: bool,
current_mode: Mode,
}
async fn build_tools(
@@ -228,6 +276,12 @@ impl SessionController {
drop(config_guard); // Release the lock before calling build_tools
let initial_mode = if enable_code_tools {
Mode::Code
} else {
Mode::Chat
};
let (tool_registry, schema_validator) = build_tools(
config_arc.clone(),
ui.clone(),
@@ -247,8 +301,9 @@ impl SessionController {
schema_validator.clone(),
);
let base_client = factory.create()?;
let permission_client = PermissionLayer::new(base_client, Arc::new(guard.clone()));
Arc::new(permission_client)
let client = Arc::new(PermissionLayer::new(base_client, Arc::new(guard.clone())));
client.set_mode(initial_mode).await?;
client
};
Ok(Self {
@@ -268,6 +323,7 @@ impl SessionController {
credential_manager,
ui,
enable_code_tools,
current_mode: initial_mode,
})
}
@@ -325,10 +381,10 @@ impl SessionController {
.expect("Consent manager mutex poisoned");
consent.grant_consent(tool_name, data_types, endpoints);
if let Some(vault) = &self.vault {
if let Err(e) = consent.persist_to_vault(vault) {
eprintln!("Warning: Failed to persist consent to vault: {}", e);
}
if let Some(vault) = &self.vault
&& let Err(e) = consent.persist_to_vault(vault)
{
eprintln!("Warning: Failed to persist consent to vault: {}", e);
}
}
@@ -347,12 +403,11 @@ impl SessionController {
consent.grant_consent_with_scope(tool_name, data_types, endpoints, scope);
// Only persist to vault for permanent consent
if is_permanent {
if let Some(vault) = &self.vault {
if let Err(e) = consent.persist_to_vault(vault) {
eprintln!("Warning: Failed to persist consent to vault: {}", e);
}
}
if is_permanent
&& let Some(vault) = &self.vault
&& let Err(e) = consent.persist_to_vault(vault)
{
eprintln!("Warning: Failed to persist consent to vault: {}", e);
}
}
@@ -489,8 +544,13 @@ impl SessionController {
};
match self.mcp_client.call_tool(call).await {
Ok(response) => {
let content: String = serde_json::from_value(response.output)?;
Ok(content)
if let Some(text) = extract_resource_content(&response.output) {
return Ok(text);
}
let formatted = serde_json::to_string_pretty(&response.output)
.unwrap_or_else(|_| response.output.to_string());
Ok(formatted)
}
Err(err) => {
log::warn!("MCP file read failed ({}); falling back to local read", err);
@@ -500,6 +560,48 @@ impl SessionController {
}
}
pub async fn read_file_with_tools(&self, path: &str) -> Result<String> {
if !self.enable_code_tools {
return Err(Error::InvalidInput(
"Code tools are disabled in chat mode. Run `:mode code` to switch.".to_string(),
));
}
let call = McpToolCall {
name: "resources/get".to_string(),
arguments: serde_json::json!({ "path": path }),
};
let response = self.mcp_client.call_tool(call).await?;
if let Some(text) = extract_resource_content(&response.output) {
Ok(text)
} else {
let formatted = serde_json::to_string_pretty(&response.output)
.unwrap_or_else(|_| response.output.to_string());
Ok(formatted)
}
}
pub fn code_tools_enabled(&self) -> bool {
self.enable_code_tools
}
pub async fn set_code_tools_enabled(&mut self, enabled: bool) -> Result<()> {
if self.enable_code_tools == enabled {
return Ok(());
}
self.enable_code_tools = enabled;
self.rebuild_tools().await
}
pub async fn set_operating_mode(&mut self, mode: Mode) -> Result<()> {
self.current_mode = mode;
let enable_code_tools = matches!(mode, Mode::Code);
self.set_code_tools_enabled(enable_code_tools).await?;
self.mcp_client.set_mode(mode).await
}
pub async fn list_dir(&self, path: &str) -> Result<Vec<String>> {
let call = McpToolCall {
name: "resources/list".to_string(),
@@ -587,7 +689,9 @@ impl SessionController {
);
let base_client = factory.create()?;
let permission_client = PermissionLayer::new(base_client, Arc::new(config.clone()));
self.mcp_client = Arc::new(permission_client);
let client = Arc::new(permission_client);
client.set_mode(self.current_mode).await?;
self.mcp_client = client;
Ok(())
}
@@ -741,7 +845,7 @@ impl SessionController {
if !streaming {
const MAX_TOOL_ITERATIONS: usize = 5;
for _iteration in 0..MAX_TOOL_ITERATIONS {
match self.provider.chat(request.clone()).await {
match self.provider.send_prompt(request.clone()).await {
Ok(response) => {
if response.message.has_tool_calls() {
self.conversation.push_message(response.message.clone());
@@ -786,7 +890,7 @@ impl SessionController {
)));
}
match self.provider.chat_stream(request).await {
match self.provider.stream_prompt(request).await {
Ok(stream) => {
let response_id = self.conversation.start_streaming_response();
Ok(SessionOutcome::Streaming {
@@ -828,6 +932,11 @@ impl SessionController {
.filter(|calls| !calls.is_empty())
}
pub fn cancel_stream(&mut self, message_id: Uuid, notice: &str) -> Result<()> {
self.conversation
.cancel_stream(message_id, notice.to_string())
}
pub async fn execute_streaming_tools(
&mut self,
_message_id: Uuid,