Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 0 additions & 78 deletions crates/config/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ use crate::upsert_user_auth_api_key;
use crate::write_atomic;
use crate::write_provider_config;

pub const DESKTOP_NETWORK_PROXY_MODE_ENV: &str = "DEVO_DESKTOP_NETWORK_PROXY_MODE";
pub const DESKTOP_NETWORK_PROXY_URL_ENV: &str = "DEVO_DESKTOP_NETWORK_PROXY_URL";
pub const DESKTOP_NETWORK_NO_PROXY_ENV: &str = "DEVO_DESKTOP_NETWORK_NO_PROXY";

/// Stores the fully normalized runtime configuration.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppConfig {
Expand Down Expand Up @@ -590,56 +586,6 @@ pub struct FileSystemAppConfigLoader {
config_folder_home: PathBuf,
/// Command-line overrides applied on top of file-backed config.
cli_overrides: toml::Value,
/// Desktop-managed runtime proxy override read from private process env.
desktop_network_proxy_env: DesktopNetworkProxyEnv,
}

/// Desktop-managed runtime network proxy override values.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DesktopNetworkProxyEnv {
mode: Option<String>,
proxy_url: Option<String>,
no_proxy: Option<String>,
}

impl DesktopNetworkProxyEnv {
pub fn from_process_env() -> Self {
Self::from_env_fn(|key| std::env::var(key))
}

pub fn from_env_fn<F>(env: F) -> Self
where
F: Fn(&str) -> Result<String, std::env::VarError>,
{
Self {
mode: env(DESKTOP_NETWORK_PROXY_MODE_ENV)
.ok()
.and_then(|value| non_empty_string(&value)),
proxy_url: env(DESKTOP_NETWORK_PROXY_URL_ENV)
.ok()
.and_then(|value| non_empty_string(&value)),
no_proxy: env(DESKTOP_NETWORK_NO_PROXY_ENV)
.ok()
.and_then(|value| non_empty_string(&value)),
}
}

pub fn custom(proxy_url: impl Into<String>, no_proxy: Option<&str>) -> Self {
let proxy_url = proxy_url.into();
Self {
mode: Some("custom".to_string()),
proxy_url: non_empty_string(&proxy_url),
no_proxy: no_proxy.and_then(non_empty_string),
}
}

pub fn off() -> Self {
Self {
mode: Some("off".to_string()),
proxy_url: None,
no_proxy: None,
}
}
}

impl FileSystemAppConfigLoader {
Expand All @@ -648,7 +594,6 @@ impl FileSystemAppConfigLoader {
Self {
config_folder_home,
cli_overrides: toml::Value::Table(Default::default()),
desktop_network_proxy_env: DesktopNetworkProxyEnv::from_process_env(),
}
}

Expand All @@ -658,11 +603,6 @@ impl FileSystemAppConfigLoader {
self
}

pub fn with_desktop_network_proxy_env(mut self, env: DesktopNetworkProxyEnv) -> Self {
self.desktop_network_proxy_env = env;
self
}

fn user_config_path(&self) -> PathBuf {
self.config_folder_home.join(APP_CONFIG_FILE_NAME)
}
Expand Down Expand Up @@ -718,29 +658,11 @@ impl AppConfigLoader for FileSystemAppConfigLoader {
message: source.to_string(),
})?;
config.provider = provider_config;
apply_desktop_network_proxy_env_override(&mut config, &self.desktop_network_proxy_env);
validate_app_config(&config)?;
Ok(config)
}
}

fn apply_desktop_network_proxy_env_override(config: &mut AppConfig, env: &DesktopNetworkProxyEnv) {
match env.mode.as_deref() {
Some("custom") => {
if let Some(proxy_url) = env.proxy_url.clone() {
config.provider_http.proxy_url = Some(proxy_url);
config.provider_http.no_proxy = env.no_proxy.clone();
}
}
Some("off") => {
config.provider_http.proxy_url = None;
config.provider_http.no_proxy = None;
}
Some("system") | None => {}
Some(_) => {}
}
}

fn merge_toml_values(base: &mut toml::Value, overlay: toml::Value) {
match (base, overlay) {
(toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
Expand Down
66 changes: 0 additions & 66 deletions crates/config/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use super::AppConfig;
use super::AppConfigLoader;
use super::AppConfigStore;
use super::CommandHookConfig;
use super::DesktopNetworkProxyEnv;
use super::ExperimentalConfig;
use super::FileSystemAppConfigLoader;
use super::HookCommandConfig;
Expand Down Expand Up @@ -476,71 +475,6 @@ invocation_method = "openai_responses"
let _ = std::fs::remove_dir_all(root);
}

/// Verifies: Desktop-managed runtime proxy env overrides file-backed provider proxy without mutating config files.
#[test]
fn loader_applies_desktop_network_proxy_custom_override() {
let root = unique_temp_dir("config-desktop-proxy-custom");
let home = root.join("home").join(".devo");
let workspace = root.join("workspace");
std::fs::create_dir_all(&home).expect("home config dir");
std::fs::create_dir_all(workspace.join(".devo")).expect("workspace config dir");
let project_config = workspace.join(".devo").join("config.toml");
std::fs::write(
&project_config,
r#"
[provider_http]
proxy_url = "http://workspace-proxy.example:8080"
"#,
)
.expect("write project config");

let loader = FileSystemAppConfigLoader::new(home).with_desktop_network_proxy_env(
DesktopNetworkProxyEnv::custom("socks5h://127.0.0.1:7890", Some("localhost,127.0.0.1,::1")),
);
let config = loader.load(Some(&workspace)).expect("load config");

assert_eq!(
config.provider_http,
ProviderHttpConfig {
proxy_url: Some("socks5h://127.0.0.1:7890".to_string()),
no_proxy: Some("localhost,127.0.0.1,::1".to_string()),
}
);
let persisted = std::fs::read_to_string(project_config).expect("read project config");
assert!(persisted.contains("workspace-proxy.example"));
}

/// Verifies: Desktop-managed runtime proxy off mode clears the effective provider proxy only for the process.
#[test]
fn loader_applies_desktop_network_proxy_off_override() {
let root = unique_temp_dir("config-desktop-proxy-off");
let home = root.join("home").join(".devo");
let workspace = root.join("workspace");
std::fs::create_dir_all(&home).expect("home config dir");
std::fs::create_dir_all(workspace.join(".devo")).expect("workspace config dir");
std::fs::write(
workspace.join(".devo").join("config.toml"),
r#"
[provider_http]
proxy_url = "http://workspace-proxy.example:8080"
no_proxy = "localhost"
"#,
)
.expect("write project config");

let loader = FileSystemAppConfigLoader::new(home)
.with_desktop_network_proxy_env(DesktopNetworkProxyEnv::off());
let config = loader.load(Some(&workspace)).expect("load config");

assert_eq!(
config.provider_http,
ProviderHttpConfig {
proxy_url: None,
no_proxy: None,
}
);
}

/// Trace: L2-DES-APP-005
/// Verifies: omitted defaulted provider fields in a higher-priority partial overlay do not overwrite lower-priority values.
#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ devo-safety = { workspace = true }
devo-skills = { workspace = true }
devo-tools = { workspace = true }
devo-util-process = { workspace = true }
devo-util-paths = { workspace = true }

anyhow = { workspace = true }
async-trait = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/core/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3604,13 +3604,15 @@ mod tests {
reasoning_effort: None,
label: "Off".into(),
description: "Use the standard model".into(),
extra_body: None,
},
ReasoningVariant {
selection_value: "enabled".into(),
model_slug: "kimi-k2.5-thinking".into(),
reasoning_effort: Some(ReasoningEffort::Medium),
label: "On".into(),
description: "Use the reasoning model".into(),
extra_body: None,
},
],
},
Expand Down
5 changes: 4 additions & 1 deletion crates/core/src/skills.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Core-facing skill catalog wrapper backed by `devo-skills`.

use devo_util_paths;
use std::path::Path;
use std::path::PathBuf;

Expand Down Expand Up @@ -104,8 +105,10 @@ pub struct FileSystemSkillCatalog {

impl FileSystemSkillCatalog {
pub fn new(config: SkillsConfig) -> Self {
let devo_home = devo_util_paths::find_devo_home()
.unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
Self::with_devo_home(config, cwd.clone(), cwd, vec![".git".to_string()])
Self::with_devo_home(config, devo_home, cwd, vec![".git".to_string()])
}

pub fn with_devo_home(
Expand Down
5 changes: 4 additions & 1 deletion crates/protocol/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ impl Model {
request_thinking: None,
request_reasoning_effort: variant.reasoning_effort,
effective_reasoning_effort: variant.reasoning_effort,
extra_body: None,
extra_body: variant.extra_body.clone(),
}
} else {
ResolvedReasoningRequest {
Expand Down Expand Up @@ -839,13 +839,15 @@ mod tests {
reasoning_effort: None,
label: String::from("Off"),
description: String::from("Use the standard model"),
extra_body: None,
},
ReasoningVariant {
selection_value: String::from("enabled"),
model_slug: String::from("kimi-k2.5-thinking"),
reasoning_effort: Some(ReasoningEffort::Medium),
label: String::from("On"),
description: String::from("Use the reasoning model"),
extra_body: None,
},
],
},
Expand Down Expand Up @@ -873,6 +875,7 @@ mod tests {
reasoning_effort: None,
label: String::from("Off"),
description: String::from("Use the standard model"),
extra_body: None,
}],
},
));
Expand Down
3 changes: 3 additions & 0 deletions crates/protocol/src/reasoning_effort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ pub struct ReasoningVariant {
pub label: String,
/// User-facing description shown alongside the label.
pub description: String,
/// Optional provider-specific JSON merged into the request body.
#[serde(default)]
pub extra_body: Option<Value>,
}

/// Fully resolved request settings derived from a logical model plus reasoning-effort selection.
Expand Down
4 changes: 3 additions & 1 deletion crates/provider/src/anthropic/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ impl AnthropicProvider {
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"));

let builder = if let Some(api_key) = &self.api_key {
builder.header("x-api-key", api_key)
builder
.header("x-api-key", api_key)
.header("Authorization", format!("Bearer {}", api_key))
} else {
builder
};
Expand Down
11 changes: 11 additions & 0 deletions crates/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ enum OnboardingStep {
struct ActiveToolCall {
tool_use_id: String,
tool_name: Option<String>,
seq: u64,
input: Option<serde_json::Value>,
title: String,
lines: Vec<Line<'static>>,
Expand Down Expand Up @@ -311,6 +312,15 @@ pub(crate) struct ChatWidget {
user_cell_history_indices: Vec<usize>,
startup_header_mascot_frame_index: usize,
startup_header_next_animation_at: Instant,
next_seq: u64,
}

impl ChatWidget {
fn reserve_seq(&mut self) -> u64 {
let seq = self.next_seq;
self.next_seq += 1;
seq
}
}

impl ChatWidget {
Expand Down Expand Up @@ -464,6 +474,7 @@ impl ChatWidget {
user_cell_history_indices: Vec::new(),
startup_header_mascot_frame_index: 0,
startup_header_next_animation_at: Instant::now() + STARTUP_HEADER_ANIMATION_INTERVAL,
next_seq: 0,
};

// Model onboarding can inject additional startup UI before the first frame is drawn.
Expand Down
4 changes: 2 additions & 2 deletions crates/tui/src/chatwidget/restored_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ impl ChatWidget {
self.add_history_entry_without_redraw(Box::new(ToolResultCell::new(
(!item.title.is_empty()).then(|| Self::ran_tool_line(&item.title)),
item.body.clone(),
self.failed_dot_prefix(),
Self::failed_dot_prefix(),
Line::from(" "),
Self::tool_text_style(),
false,
Expand Down Expand Up @@ -327,7 +327,7 @@ impl ChatWidget {
title_line: (!call_item.title.is_empty())
.then(|| Self::ran_tool_line(&call_item.title)),
dot_prefix: if result_item.kind == devo_protocol::SessionHistoryItemKind::Error {
self.failed_dot_prefix()
Self::failed_dot_prefix()
} else {
Self::tool_dot_prefix()
},
Expand Down
Loading
Loading