Apply recent changes

This commit is contained in:
2025-10-09 11:33:27 +02:00
parent d002d35bde
commit fe414d49e6
28 changed files with 2106 additions and 634 deletions

View File

@@ -155,6 +155,7 @@ pub struct ChatApp {
available_themes: Vec<String>, // Cached list of theme names
selected_theme_index: usize, // Index of selected theme in browser
pending_consent: Option<ConsentDialogState>, // Pending consent request
system_status: String, // System/status messages (tool execution, status, etc)
}
#[derive(Clone, Debug)]
@@ -173,15 +174,16 @@ impl ChatApp {
let mut textarea = TextArea::default();
configure_textarea_defaults(&mut textarea);
// Load theme based on config
let theme_name = &controller.config().ui.theme;
let theme = owlen_core::theme::get_theme(theme_name).unwrap_or_else(|| {
// Load theme and provider based on config before moving `controller`.
let config_guard = controller.config_async().await;
let theme_name = config_guard.ui.theme.clone();
let current_provider = config_guard.general.default_provider.clone();
drop(config_guard);
let theme = owlen_core::theme::get_theme(&theme_name).unwrap_or_else(|| {
eprintln!("Warning: Theme '{}' not found, using default", theme_name);
Theme::default()
});
let current_provider = controller.config().general.default_provider.clone();
let app = Self {
controller,
mode: InputMode::Normal,
@@ -225,6 +227,7 @@ impl ChatApp {
available_themes: Vec::new(),
selected_theme_index: 0,
pending_consent: None,
system_status: String::new(),
};
Ok((app, session_rx))
@@ -260,10 +263,16 @@ impl ChatApp {
self.controller.selected_model()
}
pub fn config(&self) -> &owlen_core::config::Config {
// Synchronous access for UI rendering and other callers that expect an immediate Config.
pub fn config(&self) -> tokio::sync::MutexGuard<'_, owlen_core::config::Config> {
self.controller.config()
}
// Asynchronous version retained for places that already await the config.
pub async fn config_async(&self) -> tokio::sync::MutexGuard<'_, owlen_core::config::Config> {
self.controller.config_async().await
}
pub(crate) fn model_selector_items(&self) -> &[ModelSelectorItem] {
&self.model_selector_items
}
@@ -328,6 +337,25 @@ impl ChatApp {
&mut self.textarea
}
pub fn system_status(&self) -> &str {
&self.system_status
}
pub fn set_system_status(&mut self, status: String) {
self.system_status = status;
}
pub fn append_system_status(&mut self, status: &str) {
if !self.system_status.is_empty() {
self.system_status.push_str(" | ");
}
self.system_status.push_str(status);
}
pub fn clear_system_status(&mut self) {
self.system_status.clear();
}
pub fn command_buffer(&self) -> &str {
&self.command_buffer
}
@@ -463,7 +491,7 @@ impl ChatApp {
self.theme = theme;
// Save theme to config
self.controller.config_mut().ui.theme = theme_name.to_string();
if let Err(err) = config::save_config(self.controller.config()) {
if let Err(err) = config::save_config(&self.controller.config()) {
self.error = Some(format!("Failed to save theme config: {}", err));
} else {
self.status = format!("Switched to theme: {}", theme_name);
@@ -538,10 +566,10 @@ impl ChatApp {
self.expanded_provider = Some(self.selected_provider.clone());
self.update_selected_provider_index();
self.sync_selected_model_index();
self.sync_selected_model_index().await;
// Ensure the default model is set in the controller and config
self.controller.ensure_default_model(&self.models);
// Ensure the default model is set in the controller and config (async)
self.controller.ensure_default_model(&self.models).await;
let current_model_name = self.controller.selected_model().to_string();
let current_model_provider = self.controller.config().general.default_provider.clone();
@@ -549,7 +577,7 @@ impl ChatApp {
if config_model_name.as_deref() != Some(&current_model_name)
|| config_model_provider != current_model_provider
{
if let Err(err) = config::save_config(self.controller.config()) {
if let Err(err) = config::save_config(&self.controller.config()) {
self.error = Some(format!("Failed to save config: {err}"));
} else {
self.error = None;
@@ -592,24 +620,74 @@ impl ChatApp {
// Handle consent dialog first (highest priority)
if let Some(consent_state) = &self.pending_consent {
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
// Grant consent
KeyCode::Char('1') => {
// Allow once
let tool_name = consent_state.tool_name.clone();
let data_types = consent_state.data_types.clone();
let endpoints = consent_state.endpoints.clone();
self.controller
.grant_consent(&tool_name, data_types, endpoints);
self.controller.grant_consent_with_scope(
&tool_name,
data_types,
endpoints,
owlen_core::consent::ConsentScope::Once,
);
self.pending_consent = None;
self.status = format!("✓ Consent granted for {}", tool_name);
self.status = format!("✓ Consent granted (once) for {}", tool_name);
self.set_system_status(format!(
"✓ Consent granted (once): {}",
tool_name
));
return Ok(AppState::Running);
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
KeyCode::Char('2') => {
// Allow session
let tool_name = consent_state.tool_name.clone();
let data_types = consent_state.data_types.clone();
let endpoints = consent_state.endpoints.clone();
self.controller.grant_consent_with_scope(
&tool_name,
data_types,
endpoints,
owlen_core::consent::ConsentScope::Session,
);
self.pending_consent = None;
self.status = format!("✓ Consent granted (session) for {}", tool_name);
self.set_system_status(format!(
"✓ Consent granted (session): {}",
tool_name
));
return Ok(AppState::Running);
}
KeyCode::Char('3') => {
// Allow always (permanent)
let tool_name = consent_state.tool_name.clone();
let data_types = consent_state.data_types.clone();
let endpoints = consent_state.endpoints.clone();
self.controller.grant_consent_with_scope(
&tool_name,
data_types,
endpoints,
owlen_core::consent::ConsentScope::Permanent,
);
self.pending_consent = None;
self.status =
format!("✓ Consent granted (permanent) for {}", tool_name);
self.set_system_status(format!(
"✓ Consent granted (permanent): {}",
tool_name
));
return Ok(AppState::Running);
}
KeyCode::Char('4') | KeyCode::Esc => {
// Deny consent - clear both consent and pending tool execution to prevent retry
let tool_name = consent_state.tool_name.clone();
self.pending_consent = None;
self.pending_tool_execution = None; // Clear to prevent infinite retry
self.status = format!("✗ Consent denied for {}", tool_name);
self.set_system_status(format!("✗ Consent denied: {}", tool_name));
self.error = Some(format!("Tool {} was blocked by user", tool_name));
return Ok(AppState::Running);
}
@@ -1532,7 +1610,7 @@ impl ChatApp {
match self.controller.set_tool_enabled(tool, true).await {
Ok(_) => {
if let Err(err) =
config::save_config(self.controller.config())
config::save_config(&self.controller.config())
{
self.error = Some(format!(
"Enabled {tool}, but failed to save config: {err}"
@@ -1557,7 +1635,7 @@ impl ChatApp {
match self.controller.set_tool_enabled(tool, false).await {
Ok(_) => {
if let Err(err) =
config::save_config(self.controller.config())
config::save_config(&self.controller.config())
{
self.error = Some(format!(
"Disabled {tool}, but failed to save config: {err}"
@@ -1619,7 +1697,8 @@ impl ChatApp {
self.available_providers.get(self.selected_provider_index)
{
self.selected_provider = provider.clone();
self.sync_selected_model_index(); // Update model selection based on new provider
// Update model selection based on new provider (await async)
self.sync_selected_model_index().await; // Update model selection based on new provider
self.mode = InputMode::ModelSelection;
}
}
@@ -1679,7 +1758,8 @@ impl ChatApp {
self.selected_provider = model.provider.clone();
self.update_selected_provider_index();
self.controller.set_model(model_id.clone());
// Set the selected model asynchronously
self.controller.set_model(model_id.clone()).await;
self.status = format!(
"Using model: {} (provider: {})",
model_label, self.selected_provider
@@ -1689,7 +1769,7 @@ impl ChatApp {
Some(model_id.clone());
self.controller.config_mut().general.default_provider =
self.selected_provider.clone();
match config::save_config(self.controller.config()) {
match config::save_config(&self.controller.config()) {
Ok(_) => self.error = None,
Err(err) => {
self.error = Some(format!(
@@ -2351,7 +2431,9 @@ impl ChatApp {
let provider_cfg = if let Some(cfg) = self.controller.config().provider(provider_name) {
cfg.clone()
} else {
let cfg = config::ensure_provider_config(self.controller.config_mut(), provider_name);
let mut guard = self.controller.config_mut();
// Pass a mutable reference directly; avoid unnecessary deref
let cfg = config::ensure_provider_config(&mut guard, provider_name);
cfg.clone()
};
@@ -2403,8 +2485,9 @@ impl ChatApp {
self.expanded_provider = Some(self.selected_provider.clone());
self.update_selected_provider_index();
self.controller.ensure_default_model(&self.models);
self.sync_selected_model_index();
// Ensure the default model is set after refreshing models (async)
self.controller.ensure_default_model(&self.models).await;
self.sync_selected_model_index().await;
let current_model_name = self.controller.selected_model().to_string();
let current_model_provider = self.controller.config().general.default_provider.clone();
@@ -2412,7 +2495,7 @@ impl ChatApp {
if config_model_name.as_deref() != Some(&current_model_name)
|| config_model_provider != current_model_provider
{
if let Err(err) = config::save_config(self.controller.config()) {
if let Err(err) = config::save_config(&self.controller.config()) {
self.error = Some(format!("Failed to save config: {err}"));
} else {
self.error = None;
@@ -2537,6 +2620,13 @@ impl ChatApp {
let consent_needed = self.controller.check_tools_consent_needed(&tool_calls);
if !consent_needed.is_empty() {
// If a consent dialog is already being shown, don't send another request
// Just re-queue the tool execution and wait for user response
if self.pending_consent.is_some() {
self.pending_tool_execution = Some((message_id, tool_calls));
return Ok(());
}
// Show consent for the first tool that needs it
// After consent is granted, the next iteration will check remaining tools
let (tool_name, data_types, endpoints) = consent_needed.into_iter().next().unwrap();
@@ -2555,6 +2645,11 @@ impl ChatApp {
// Show tool execution status
self.status = format!("🔧 Executing {} tool(s)...", tool_calls.len());
// Show tool names in system output
let tool_names: Vec<String> = tool_calls.iter().map(|tc| tc.name.clone()).collect();
self.set_system_status(format!("🔧 Executing tools: {}", tool_names.join(", ")));
self.start_loading_animation();
// Execute tools and get the result
@@ -2569,6 +2664,7 @@ impl ChatApp {
}) => {
// Tool execution succeeded, spawn stream handler for continuation
self.status = "Tool results sent. Generating response...".to_string();
self.set_system_status("✓ Tools executed successfully".to_string());
self.spawn_stream(response_id, stream);
match self.controller.mark_stream_placeholder(response_id, "") {
Ok(_) => self.error = None,
@@ -2582,19 +2678,22 @@ impl ChatApp {
// Tool execution complete without streaming (shouldn't happen in streaming mode)
self.stop_loading_animation();
self.status = "✓ Tool execution complete".to_string();
self.set_system_status("✓ Tool execution complete".to_string());
self.error = None;
Ok(())
}
Err(err) => {
self.stop_loading_animation();
self.status = "Tool execution failed".to_string();
self.set_system_status(format!("❌ Tool execution failed: {}", err));
self.error = Some(format!("Tool execution failed: {}", err));
Ok(())
}
}
}
fn sync_selected_model_index(&mut self) {
// Updated to async to allow awaiting async controller calls
async fn sync_selected_model_index(&mut self) {
self.expanded_provider = Some(self.selected_provider.clone());
self.rebuild_model_selector_items();
@@ -2616,7 +2715,8 @@ impl ChatApp {
if let Some(model) = self.selected_model_info().cloned() {
self.selected_provider = model.provider.clone();
self.controller.set_model(model.id.clone());
// Set the selected model asynchronously
self.controller.set_model(model.id.clone()).await;
self.controller.config_mut().general.default_model = Some(model.id.clone());
self.controller.config_mut().general.default_provider =
self.selected_provider.clone();
@@ -2627,7 +2727,7 @@ impl ChatApp {
self.update_selected_provider_index();
if config_updated {
if let Err(err) = config::save_config(self.controller.config()) {
if let Err(err) = config::save_config(&self.controller.config()) {
self.error = Some(format!("Failed to save config: {err}"));
} else {
self.error = None;