fix(desktop): DeerFlow UI — ChatArea refactor + ai-elements + dead CSS cleanup
ChatArea retry button uses setInput instead of direct sendToGateway, fix bootstrap spinner stuck for non-logged-in users, remove dead CSS (aurora-title/sidebar-open/quick-action-chips), add ai components (ReasoningBlock/StreamingText/ChatMode/ModelSelector/TaskProgress), add ClassroomPlayer + ResizableChatLayout + artifact panel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -249,3 +249,130 @@ pub async fn kernel_shutdown(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply SaaS-synced configuration to the Kernel config file.
|
||||
///
|
||||
/// Writes relevant config values (agent, llm categories) to the TOML config file.
|
||||
/// The changes take effect on the next Kernel restart.
|
||||
#[tauri::command]
|
||||
pub async fn kernel_apply_saas_config(
|
||||
configs: Vec<SaasConfigItem>,
|
||||
) -> Result<u32, String> {
|
||||
use std::io::Write;
|
||||
|
||||
let config_path = zclaw_kernel::config::KernelConfig::find_config_path()
|
||||
.ok_or_else(|| "No config file path found".to_string())?;
|
||||
|
||||
// Read existing config or create empty
|
||||
let existing = if config_path.exists() {
|
||||
std::fs::read_to_string(&config_path).unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let mut updated = existing;
|
||||
let mut applied: u32 = 0;
|
||||
|
||||
for config in &configs {
|
||||
// Only process kernel-relevant categories
|
||||
if !matches!(config.category.as_str(), "agent" | "llm") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Write key=value to the [llm] or [agent] section
|
||||
let section = &config.category;
|
||||
let key = config.key.replace('.', "_");
|
||||
let value = &config.value;
|
||||
|
||||
// Simple TOML patching: find or create section, update key
|
||||
let section_header = format!("[{}]", section);
|
||||
let line_to_set = format!("{} = {}", key, toml_quote_value(value));
|
||||
|
||||
if let Some(section_start) = updated.find(§ion_header) {
|
||||
// Section exists, find or add the key within it
|
||||
let after_header = section_start + section_header.len();
|
||||
let next_section = updated[after_header..].find("\n[")
|
||||
.map(|i| after_header + i)
|
||||
.unwrap_or(updated.len());
|
||||
|
||||
let section_content = &updated[after_header..next_section];
|
||||
let key_prefix = format!("\n{} =", key);
|
||||
let key_prefix_alt = format!("\n{}=", key);
|
||||
|
||||
if let Some(key_pos) = section_content.find(&key_prefix)
|
||||
.or_else(|| section_content.find(&key_prefix_alt))
|
||||
{
|
||||
// Key exists, replace the line
|
||||
let line_start = after_header + key_pos + 1; // skip \n
|
||||
let line_end = updated[line_start..].find('\n')
|
||||
.map(|i| line_start + i)
|
||||
.unwrap_or(updated.len());
|
||||
updated = format!(
|
||||
"{}{}{}\n{}",
|
||||
&updated[..line_start],
|
||||
line_to_set,
|
||||
if line_end < updated.len() { "" } else { "" },
|
||||
&updated[line_end..]
|
||||
);
|
||||
// Remove the extra newline if line_end included one
|
||||
updated = updated.replace(&format!("{}\n\n", line_to_set), &format!("{}\n", line_to_set));
|
||||
} else {
|
||||
// Key doesn't exist, append to section
|
||||
updated.insert_str(next_section, format!("\n{}", line_to_set).as_str());
|
||||
}
|
||||
} else {
|
||||
// Section doesn't exist, append it
|
||||
updated = format!("{}\n{}\n{}\n", updated.trim_end(), section_header, line_to_set);
|
||||
}
|
||||
applied += 1;
|
||||
}
|
||||
|
||||
if applied > 0 {
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = config_path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| format!("Failed to create config dir: {}", e))?;
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::create(&config_path)
|
||||
.map_err(|e| format!("Failed to write config: {}", e))?;
|
||||
file.write_all(updated.as_bytes())
|
||||
.map_err(|e| format!("Failed to write config: {}", e))?;
|
||||
|
||||
tracing::info!(
|
||||
"[kernel_apply_saas_config] Applied {} config items to {:?} (restart required)",
|
||||
applied,
|
||||
config_path
|
||||
);
|
||||
}
|
||||
|
||||
Ok(applied)
|
||||
}
|
||||
|
||||
/// Single config item from SaaS sync
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SaasConfigItem {
|
||||
pub category: String,
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Quote a value for TOML format
|
||||
fn toml_quote_value(value: &str) -> String {
|
||||
// Try to parse as number or boolean
|
||||
if value == "true" || value == "false" {
|
||||
return value.to_string();
|
||||
}
|
||||
if let Ok(n) = value.parse::<i64>() {
|
||||
return n.to_string();
|
||||
}
|
||||
if let Ok(n) = value.parse::<f64>() {
|
||||
return n.to_string();
|
||||
}
|
||||
// Handle multi-line strings with TOML triple-quote syntax
|
||||
if value.contains('\n') {
|
||||
return format!("\"\"\"\n{}\"\"\"", value.replace('\\', "\\\\").replace("\"\"\"", "'\"'\"'\""));
|
||||
}
|
||||
// Default: quote as string
|
||||
format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user