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
20 changes: 10 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ jobs:
components: rustfmt
- run: cargo fmt --all -- --check

# clippy:
# name: Clippy
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: dtolnay/rust-toolchain@stable
# with:
# components: clippy
# - uses: Swatinem/rust-cache@v2
# - run: cargo clippy --workspace --all-targets -- -D warnings
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace --all-targets -- -D warnings

doc:
name: Doc
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
.DS_Store
.claude
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This repository is a Rust-based coding agent, currently called `devo`.
- When running Rust-related commands (e.g., `just fix` or `cargo test`), allow them to complete without interruption, do not try to kill them using the PID. Slow execution due to Rust’s locking behavior is expected.
- Agent can not apply patch to a file more than 800 lines, cause windows patch length limit, agent would failed to apply patch.
- Do not introduce trivial wrapper functions — call the underlying function directly unless reuse or abstraction is clearly justified.
- Exclude the target folder when using glob or grep, since it contains a large number of Rust build artifacts.

## Tests

Expand Down
34 changes: 20 additions & 14 deletions Cargo.lock

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

12 changes: 12 additions & 0 deletions crates/cli/src/agent_command.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use anyhow::Context;
use anyhow::Result;
use devo_core::AppConfigLoader;
use devo_core::FileSystemAppConfigLoader;
use devo_core::ModelCatalog;
use devo_core::PresetModelCatalog;
use devo_core::ProviderConfigFile;
use devo_core::ResolvedProviderSettings;
use devo_core::load_config;
use devo_core::project_config_key;
use devo_core::resolve_provider_settings;
use devo_protocol::PermissionPreset;
use devo_protocol::ProviderWireApi;
use devo_tui::InitialTuiSession;
use devo_tui::InteractiveTuiConfig;
Expand All @@ -24,6 +28,13 @@ pub(crate) async fn run_agent(force_onboarding: bool, log_level: Option<&str>) -
let config_home = find_devo_home().context("could not determine devo home directory")?;
let model_catalog = PresetModelCatalog::load_from_config(&config_home, Some(&cwd))?;
let stored_config = load_config().unwrap_or_default();
let app_config = FileSystemAppConfigLoader::new(config_home.clone()).load(Some(&cwd))?;
let project_key = project_config_key(&cwd);
let permission_preset = app_config
.projects
.get(&project_key)
.and_then(|config| config.permission_preset)
.unwrap_or(PermissionPreset::Default);
let (onboarding_mode, resolved) =
resolve_initial_provider_settings(force_onboarding, &stored_config, &model_catalog)?;

Expand All @@ -47,6 +58,7 @@ pub(crate) async fn run_agent(force_onboarding: bool, log_level: Option<&str>) -
model,
provider: wire_api,
thinking_selection: model_thinking_selection,
permission_preset,
// TODO: why do we need cwd here, maybe remove it ?
cwd,
},
Expand Down
4 changes: 1 addition & 3 deletions crates/cli/src/prompt_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ pub(crate) async fn run_prompt(
use devo_core::SessionConfig;
use devo_core::SessionState;
use devo_core::default_base_instructions;
use devo_tools::ToolRegistry;
use devo_tools::ToolRuntime;

let cwd = std::env::current_dir()?;
Expand Down Expand Up @@ -48,8 +47,7 @@ pub(crate) async fn run_prompt(
session_state.push_message(devo_core::Message::user(input.to_string()));

let registry = {
let mut reg = ToolRegistry::new();
devo_tools::register_builtin_tools(&mut reg);
let reg = devo_tools::create_default_tool_registry();
std::sync::Arc::new(reg)
};
let runtime = ToolRuntime::new_without_permissions(std::sync::Arc::clone(&registry));
Expand Down
15 changes: 15 additions & 0 deletions crates/client/src/stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::sync::atomic::Ordering;

use anyhow::Context;
use anyhow::Result;
use devo_protocol::ApprovalRespondParams;
use devo_protocol::ClientNotification;
use devo_protocol::ClientRequest;
use devo_protocol::ClientTransportKind;
Expand All @@ -28,6 +29,8 @@ use devo_protocol::SessionListParams;
use devo_protocol::SessionListResult;
use devo_protocol::SessionMetadataUpdateParams;
use devo_protocol::SessionMetadataUpdateResult;
use devo_protocol::SessionPermissionsUpdateParams;
use devo_protocol::SessionPermissionsUpdateResult;
use devo_protocol::SessionResumeParams;
use devo_protocol::SessionResumeResult;
use devo_protocol::SessionRollbackParams;
Expand Down Expand Up @@ -184,6 +187,13 @@ impl StdioServerClient {
self.request("session/metadata/update", params).await
}

pub async fn session_permissions_update(
&mut self,
params: SessionPermissionsUpdateParams,
) -> Result<SessionPermissionsUpdateResult> {
self.request("session/permissions/update", params).await
}

pub async fn session_compact(
&mut self,
params: SessionCompactParams,
Expand Down Expand Up @@ -239,6 +249,11 @@ impl StdioServerClient {
self.request("turn/steer", params).await
}

pub async fn approval_respond(&mut self, params: ApprovalRespondParams) -> Result<()> {
let _: serde_json::Value = self.request("approval/respond", params).await?;
Ok(())
}

pub async fn recv_notification(&mut self) -> Option<ServerNotificationMessage> {
self.notifications_rx.recv().await
}
Expand Down
35 changes: 35 additions & 0 deletions crates/core/src/config/app.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};

use devo_protocol::PermissionPreset;
use serde::{Deserialize, Serialize};

use devo_utils::FileSystemConfigPathResolver;
use devo_utils::git_op::get_git_repo_root;

use crate::AgentsMdConfig;
use crate::SkillsConfig;
Expand Down Expand Up @@ -35,6 +38,15 @@ pub struct AppConfig {
/// TODO: Not sure what's purpose of `project_root_markers`?
/// Marker names used when discovering a project root.
pub project_root_markers: Vec<String>,
/// User-level settings remembered per project key.
pub projects: BTreeMap<String, ProjectConfig>,
}

/// Settings remembered for one project.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ProjectConfig {
/// Permission preset to use when starting new sessions for this project.
pub permission_preset: Option<PermissionPreset>,
}

/// Controls how the CLI checks for new releases at startup.
Expand Down Expand Up @@ -118,6 +130,7 @@ impl Default for AppConfig {
check_interval_hours: 24,
},
project_root_markers: vec![".git".into()],
projects: BTreeMap::new(),
}
}
}
Expand All @@ -131,6 +144,28 @@ impl AppConfig {
}
}

/// Returns the stable key used to remember project-level permission settings.
///
/// Git repositories are keyed by their repository root. Non-git directories fall
/// back to the canonical current working directory when possible.
pub fn project_config_key(cwd: &Path) -> String {
let root = get_git_repo_root(cwd)
.or_else(|| cwd.canonicalize().ok())
.unwrap_or_else(|| cwd.to_path_buf());
strip_unc_prefix(root).display().to_string()
}

fn strip_unc_prefix(path: PathBuf) -> PathBuf {
#[cfg(windows)]
{
let value = path.display().to_string();
if let Some(stripped) = value.strip_prefix("\\\\?\\") {
return PathBuf::from(stripped);
}
}
path
}

fn read_config_value(path: &Path) -> Result<toml::Value, AppConfigError> {
let contents = fs::read_to_string(path).map_err(|source| AppConfigError::Io {
path: path.to_path_buf(),
Expand Down
Loading
Loading