Skip to content
Open
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
2 changes: 2 additions & 0 deletions src-tauri/src/acp/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,8 @@ pub struct AcpAgentInfo {
pub host_tools_agent_mode: bool,
pub config_json: Option<String>,
pub config_file_path: Option<String>,
pub system_instructions_file_path: Option<String>,
pub system_instructions_content: Option<String>,
pub codex_auth_json: Option<String>,
pub codex_config_toml: Option<String>,
/// Compact structured codex model-catalog source (the `codeg` custom-model
Expand Down
101 changes: 101 additions & 0 deletions src-tauri/src/commands/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9223,6 +9223,70 @@ fn qoder_settings_json_path() -> PathBuf {
crate::parsers::qoder::resolve_qoder_config_dir().join("settings.json")
}


pub(crate) fn agent_system_instructions_path(agent_type: AgentType) -> Option<PathBuf> {
match agent_type {
AgentType::ClaudeCode => Some(home_dir_or_default().join(".claude").join("CLAUDE.md")),
AgentType::Codex => Some(crate::parsers::codex::resolve_codex_home_dir().join("AGENTS.md")),
AgentType::Gemini => Some(crate::parsers::gemini::resolve_gemini_base_dir().join("GEMINI.md")),
AgentType::OpenCode => Some(
crate::acp::opencode_config::config_dir()
.unwrap_or_else(|| home_dir_or_default().join(".config").join("opencode"))
.join("AGENTS.md"),
),
AgentType::Antigravity => Some(crate::parsers::antigravity::resolve_gemini_home().join("GEMINI.md")),
AgentType::Cline => Some(home_dir_or_default().join(".cline").join("AGENTS.md")),
AgentType::Hermes => Some(crate::parsers::hermes::resolve_hermes_home_dir().join("AGENTS.md")),
AgentType::CodeBuddy => Some(crate::parsers::codebuddy::resolve_codebuddy_config_dir().join("AGENTS.md")),
AgentType::KimiCode => Some(crate::parsers::kimi_code::resolve_kimi_code_home_dir().join("AGENTS.md")),
AgentType::Pi => Some(crate::parsers::pi::resolve_pi_agent_dir().join("AGENTS.md")),
AgentType::Grok => Some(crate::parsers::grok::resolve_grok_home_dir().join("AGENTS.md")),
AgentType::Cursor => Some(crate::parsers::cursor::resolve_cursor_config_dir().join("AGENTS.md")),
AgentType::DeepSeek => Some(home_dir_or_default().join(".dsh").join("AGENTS.md")),
AgentType::Qoder => Some(crate::parsers::qoder::resolve_qoder_config_dir().join("AGENTS.md")),
AgentType::Kiro => Some(crate::parsers::kiro::resolve_kiro_home_dir().join("AGENTS.md")),
AgentType::Trae => Some(crate::acp::trae::resolve_trae_home_dir().join("AGENTS.md")),
AgentType::CommandCode => Some(crate::acp::command_code::command_code_home_dir().join("AGENTS.md")),
AgentType::ZCode => Some(inherited_node_home_dir().join(".zcode").join("AGENTS.md")),
AgentType::MiniMaxCode => Some(home_dir_or_default().join(".minimax-code").join("AGENTS.md")),
AgentType::OpenClaw => Some(home_dir_or_default().join(".openclaw").join("AGENTS.md")),
AgentType::Custom(id) => {
crate::acp::custom_registry::skills_decl(id)
.dir
.map(PathBuf::from)
.or_else(|| Some(home_dir_or_default().join(format!(".{}", id))))
.map(|p| p.join("AGENTS.md"))
}
}
}

pub(crate) fn load_agent_system_instructions(agent_type: AgentType) -> (Option<String>, Option<String>) {
let path = agent_system_instructions_path(agent_type);
let path_str = path.as_ref().map(|p| p.display().to_string());
let content = path.and_then(|p| fs::read_to_string(p).ok());
(path_str, content)
}

pub(crate) fn persist_agent_system_instructions(agent_type: AgentType, content: Option<&str>) -> Result<(), AcpError> {
let Some(path) = agent_system_instructions_path(agent_type) else {
return Ok(());
};
if let Some(text) = content {
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent).map_err(|e| {
AcpError::protocol(format!("failed to create instructions directory {}: {e}", parent.display()))
})?;
}
}
fs::write(&path, text).map_err(|e| {
AcpError::protocol(format!("failed to write instructions file {}: {e}", path.display()))
})?;
}
Ok(())
}


pub(crate) fn load_agent_local_config_json(agent_type: AgentType) -> Option<String> {
if agent_type == AgentType::Codex {
return load_codex_local_config_json();
Expand Down Expand Up @@ -12534,6 +12598,8 @@ pub(crate) async fn acp_list_agents_core(db: &AppDatabase) -> Result<Vec<AcpAgen
} else {
(local_config_json, None)
};
let (system_instructions_file_path, system_instructions_content) =
load_agent_system_instructions(agent_type);
let grok_config_toml = if agent_type == AgentType::Grok {
load_grok_config_toml_raw()
} else {
Expand Down Expand Up @@ -12608,6 +12674,8 @@ pub(crate) async fn acp_list_agents_core(db: &AppDatabase) -> Result<Vec<AcpAgen
config_json,
config_file_path: agent_local_config_path(agent_type)
.map(|path| path.display().to_string()),
system_instructions_file_path,
system_instructions_content,
codex_auth_json,
codex_config_toml,
codex_sandbox_settings,
Expand Down Expand Up @@ -12976,6 +13044,7 @@ pub async fn acp_update_agent_env(
pub(crate) async fn acp_update_agent_config_core(
agent_type: AgentType,
config_json: Option<String>,
system_instructions_content: Option<String>,
codex_auth_json: Option<String>,
codex_config_toml: Option<String>,
codex_config_toml_baseline: Option<String>,
Expand All @@ -12988,6 +13057,9 @@ pub(crate) async fn acp_update_agent_config_core(
db: &AppDatabase,
emitter: &EventEmitter,
) -> Result<(), AcpError> {
if let Some(instructions) = system_instructions_content.as_deref() {
persist_agent_system_instructions(agent_type, Some(instructions))?;
}
let config_json = config_json.and_then(|raw| {
let trimmed = raw.trim();
if trimmed.is_empty() {
Expand Down Expand Up @@ -13137,6 +13209,7 @@ pub(crate) async fn acp_update_agent_config_core(
pub(crate) async fn acp_update_agent_config_and_refresh(
agent_type: AgentType,
config_json: Option<String>,
system_instructions_content: Option<String>,
codex_auth_json: Option<String>,
codex_config_toml: Option<String>,
codex_config_toml_baseline: Option<String>,
Expand All @@ -13154,6 +13227,7 @@ pub(crate) async fn acp_update_agent_config_and_refresh(
acp_update_agent_config_core(
agent_type,
config_json,
system_instructions_content,
codex_auth_json,
codex_config_toml,
codex_config_toml_baseline,
Expand All @@ -13176,6 +13250,7 @@ pub(crate) async fn acp_update_agent_config_and_refresh(
pub async fn acp_update_agent_config(
agent_type: AgentType,
config_json: Option<String>,
system_instructions_content: Option<String>,
codex_auth_json: Option<String>,
codex_config_toml: Option<String>,
codex_config_toml_baseline: Option<String>,
Expand All @@ -13198,6 +13273,7 @@ pub async fn acp_update_agent_config(
acp_update_agent_config_and_refresh(
agent_type,
config_json,
system_instructions_content,
codex_auth_json,
codex_config_toml,
codex_config_toml_baseline,
Expand Down Expand Up @@ -23205,4 +23281,29 @@ model = "gpt"
);
assert_eq!(env.get("CLINE_PROVIDER").map(String::as_str), Some("cline-pass"));
}

#[test]
fn test_agent_system_instructions_paths() {
assert!(agent_system_instructions_path(AgentType::ClaudeCode).unwrap().ends_with("CLAUDE.md"));
assert!(agent_system_instructions_path(AgentType::CodeBuddy).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::Gemini).unwrap().ends_with("GEMINI.md"));
assert!(agent_system_instructions_path(AgentType::Antigravity).unwrap().ends_with("GEMINI.md"));
assert!(agent_system_instructions_path(AgentType::Codex).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::OpenCode).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::Cline).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::Hermes).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::Grok).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::Cursor).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::DeepSeek).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::Qoder).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::Kiro).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::Trae).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::CommandCode).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::ZCode).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::MiniMaxCode).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::KimiCode).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::Pi).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::OpenClaw).unwrap().ends_with("AGENTS.md"));
assert!(agent_system_instructions_path(AgentType::Custom("my-bot")).unwrap().ends_with("AGENTS.md"));
}
}
2 changes: 2 additions & 0 deletions src-tauri/src/web/handlers/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ pub async fn acp_update_agent_env(
pub struct AcpUpdateAgentConfigParams {
pub agent_type: AgentType,
pub config_json: Option<String>,
pub system_instructions_content: Option<String>,
pub codex_auth_json: Option<String>,
pub codex_config_toml: Option<String>,
#[serde(default)]
Expand All @@ -730,6 +731,7 @@ pub async fn acp_update_agent_config(
let affected = acp_commands::acp_update_agent_config_and_refresh(
params.agent_type,
params.config_json,
params.system_instructions_content,
params.codex_auth_json,
params.codex_config_toml,
params.codex_config_toml_baseline,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import { NextIntlClientProvider } from "next-intl"
import { describe, expect, it, vi } from "vitest"

import enMessages from "@/i18n/messages/en.json"
import {
acpListAgents,
acpUpdateAgentConfig,
listModelProviders,
} from "@/lib/api"
import type { AcpAgentInfo } from "@/lib/types"
import { AcpAgentSettings } from "./acp-agent-settings"

vi.mock("@/lib/api", async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>()
return Object.fromEntries(
Object.entries(actual).map(([name, value]) => [
name,
typeof value === "function" ? vi.fn(() => new Promise(() => {})) : value,
])
)
})
vi.mock("next/navigation", () => ({
useSearchParams: () => new URLSearchParams(),
}))

function makeAgent(overrides: Partial<AcpAgentInfo> = {}): AcpAgentInfo {
return {
agent_type: "gemini",
name: "Gemini",
registry_id: "gemini",
registry_version: "0.1.0",
installed_version: "0.1.0",
description: "Google Gemini CLI agent",
skills_capable: true,
supports_custom_version: false,
available: true,
distribution_type: "npm",
is_acp_adapter: false,
enabled: true,
sort_order: 0,
env: {},
config_json: null,
config_file_path: null,
system_instructions_file_path: "C:/Users/test/.gemini/GEMINI.md",
system_instructions_content: "# You are a helpful assistant",
codex_auth_json: null,
codex_config_toml: null,
codex_model_catalog: null,
codex_sandbox_settings: null,
cline_secrets_json: null,
hermes_config_yaml: null,
grok_config_toml: null,
grok_settings: null,
cursor_cli_config_json: null,
cursor_settings: null,
model_provider_id: null,
host_tools_agent_mode: false,
custom_source: null,
icon_url: null,
...overrides,
}
}

describe("System Instructions in ACP agent settings", () => {
it("renders system instructions with path and content, and allows saving", async () => {
const agent = makeAgent()
vi.mocked(acpListAgents).mockResolvedValue([agent])
vi.mocked(listModelProviders).mockResolvedValue([])
vi.mocked(acpUpdateAgentConfig).mockResolvedValue(0)

render(
<NextIntlClientProvider locale="en" messages={enMessages}>
<AcpAgentSettings />
</NextIntlClientProvider>
)

await screen.findByRole("heading", { name: "Gemini" })

// Check system instructions section
expect(screen.getByText("System Instructions")).toBeInTheDocument()
expect(
screen.getByText("C:/Users/test/.gemini/GEMINI.md")
).toBeInTheDocument()

const instructionsInput = screen.getByPlaceholderText(
enMessages.AcpAgentSettings.systemInstructions.placeholder
)
expect(instructionsInput).toHaveValue("# You are a helpful assistant")

// Edit instructions
fireEvent.change(instructionsInput, {
target: { value: "# Updated guidelines\nAlways write tests." },
})

// Click save system instructions
const saveButton = screen.getByRole("button", {
name: enMessages.AcpAgentSettings.actions.saveSystemInstructions,
})
fireEvent.click(saveButton)

await waitFor(() => expect(acpUpdateAgentConfig).toHaveBeenCalledTimes(1))
expect(acpUpdateAgentConfig).toHaveBeenCalledWith("gemini", {
system_instructions_content: "# Updated guidelines\nAlways write tests.",
})
})

it("does not render system instructions section when path is null", async () => {
const agent = makeAgent({
system_instructions_file_path: null,
system_instructions_content: null,
})
vi.mocked(acpListAgents).mockResolvedValue([agent])
vi.mocked(listModelProviders).mockResolvedValue([])

render(
<NextIntlClientProvider locale="en" messages={enMessages}>
<AcpAgentSettings />
</NextIntlClientProvider>
)

await screen.findByRole("heading", { name: "Gemini" })
expect(screen.queryByText("System Instructions")).not.toBeInTheDocument()
})
})
Loading
Loading