From 0822b3ee78c67f6cebadddaf1bdeea1a06a77c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=AC=E9=A3=8E=E5=90=B9=E9=9B=A8?= <2686367411@qq.com> Date: Thu, 29 Oct 2026 19:43:10 +0800 Subject: [PATCH 1/3] feat(acp): support configuring global system instructions for all agents --- src-tauri/src/acp/types.rs | 2 + src-tauri/src/commands/acp.rs | 101 ++++++++++++++ src-tauri/src/web/handlers/acp.rs | 2 + ...gent-settings.system-instructions.test.tsx | 125 ++++++++++++++++++ .../settings/acp-agent-settings.tsx | 114 +++++++++++++++- src/i18n/messages/en.json | 9 ++ src/i18n/messages/zh-CN.json | 9 ++ src/lib/api.ts | 2 + src/lib/types.ts | 2 + 9 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 src/components/settings/acp-agent-settings.system-instructions.test.tsx diff --git a/src-tauri/src/acp/types.rs b/src-tauri/src/acp/types.rs index 44a53ff4c0..2217b6d830 100644 --- a/src-tauri/src/acp/types.rs +++ b/src-tauri/src/acp/types.rs @@ -1727,6 +1727,8 @@ pub struct AcpAgentInfo { pub host_tools_agent_mode: bool, pub config_json: Option, pub config_file_path: Option, + pub system_instructions_file_path: Option, + pub system_instructions_content: Option, pub codex_auth_json: Option, pub codex_config_toml: Option, /// Compact structured codex model-catalog source (the `codeg` custom-model diff --git a/src-tauri/src/commands/acp.rs b/src-tauri/src/commands/acp.rs index 7e2a47f817..217a5e91fd 100644 --- a/src-tauri/src/commands/acp.rs +++ b/src-tauri/src/commands/acp.rs @@ -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 { + 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, Option) { + 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 { if agent_type == AgentType::Codex { return load_codex_local_config_json(); @@ -12534,6 +12598,8 @@ pub(crate) async fn acp_list_agents_core(db: &AppDatabase) -> Result Result, + system_instructions_content: Option, codex_auth_json: Option, codex_config_toml: Option, codex_config_toml_baseline: Option, @@ -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() { @@ -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, + system_instructions_content: Option, codex_auth_json: Option, codex_config_toml: Option, codex_config_toml_baseline: Option, @@ -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, @@ -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, + system_instructions_content: Option, codex_auth_json: Option, codex_config_toml: Option, codex_config_toml_baseline: Option, @@ -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, @@ -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")); + } } diff --git a/src-tauri/src/web/handlers/acp.rs b/src-tauri/src/web/handlers/acp.rs index 73ebfc6b2c..92fb326899 100644 --- a/src-tauri/src/web/handlers/acp.rs +++ b/src-tauri/src/web/handlers/acp.rs @@ -710,6 +710,7 @@ pub async fn acp_update_agent_env( pub struct AcpUpdateAgentConfigParams { pub agent_type: AgentType, pub config_json: Option, + pub system_instructions_content: Option, pub codex_auth_json: Option, pub codex_config_toml: Option, #[serde(default)] @@ -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, diff --git a/src/components/settings/acp-agent-settings.system-instructions.test.tsx b/src/components/settings/acp-agent-settings.system-instructions.test.tsx new file mode 100644 index 0000000000..e7e8ef0e4c --- /dev/null +++ b/src/components/settings/acp-agent-settings.system-instructions.test.tsx @@ -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>() + 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 { + 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( + + + + ) + + 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( + + + + ) + + await screen.findByRole("heading", { name: "Gemini" }) + expect(screen.queryByText("System Instructions")).not.toBeInTheDocument() + }) +}) diff --git a/src/components/settings/acp-agent-settings.tsx b/src/components/settings/acp-agent-settings.tsx index c02ea3bc61..0cafa951a6 100644 --- a/src/components/settings/acp-agent-settings.tsx +++ b/src/components/settings/acp-agent-settings.tsx @@ -237,6 +237,7 @@ type ClaudeAuthMode = (typeof CLAUDE_AUTH_MODES)[number] interface AgentDraft { enabled: boolean envText: string + systemInstructionsText: string configText: string apiBaseUrl: string apiKey: string @@ -3885,6 +3886,7 @@ function buildAgentDraft(agent: AcpAgentInfo): AgentDraft { return { enabled: agent.enabled, envText, + systemInstructionsText: agent.system_instructions_content ?? "", configText, apiBaseUrl: agent.agent_type === "hermes" @@ -4851,6 +4853,9 @@ export function AcpAgentSettings() { const [savingConfig, setSavingConfig] = useState< Partial> >({}) + const [savingInstructions, setSavingInstructions] = useState< + Partial> + >({}) const [modelProviders, setModelProviders] = useState([]) const [uninstallConfirmAgent, setUninstallConfirmAgent] = useState(null) @@ -5342,11 +5347,46 @@ export function AcpAgentSettings() { [reportAffectedSessions] ) + const persistSystemInstructions = useCallback( + async (agentType: AgentType, content: string) => { + setSavingInstructions((prev) => ({ ...prev, [agentType]: true })) + try { + const affected = await acpUpdateAgentConfig(agentType, { + system_instructions_content: content, + }) + reportAffectedSessions(affected) + setAgents((prev) => + prev.map((agent) => + agent.agent_type === agentType + ? { + ...agent, + system_instructions_content: content, + } + : agent + ) + ) + toast.success(t("toasts.systemInstructionsSaved"), { + description: t("toasts.configSavedHint"), + }) + } catch (err) { + console.error("[Settings] save system instructions failed:", err) + const message = toErrorMessage(err) + toast.error(t("toasts.saveSystemInstructionsFailed"), { + description: message, + }) + } finally { + setSavingInstructions((prev) => ({ ...prev, [agentType]: false })) + } + }, + [reportAffectedSessions, t] + ) + const persistConfig = useCallback( async ( agentType: AgentType, configText: string, options?: { + systemInstructionsText?: string codexAuthJsonText?: string codexConfigTomlText?: string codexConfigTomlBaseline?: string @@ -5398,6 +5438,10 @@ export function AcpAgentSettings() { try { const affected = await acpUpdateAgentConfig(agentType, { config_json: configForPersist || null, + system_instructions_content: + typeof options?.systemInstructionsText === "string" + ? options.systemInstructionsText + : null, codex_auth_json: typeof codexAuthJsonText === "string" ? codexAuthJsonText : null, codex_config_toml: @@ -5426,6 +5470,10 @@ export function AcpAgentSettings() { ? { ...agent, config_json: normalizedConfig || null, + system_instructions_content: + typeof options?.systemInstructionsText === "string" + ? options.systemInstructionsText + : agent.system_instructions_content, codex_auth_json: typeof codexAuthJsonText === "string" ? codexAuthJsonText @@ -6249,6 +6297,9 @@ export function AcpAgentSettings() { const selectedIsSavingConfig = selectedAgent ? Boolean(savingConfig[selectedAgent.agent_type]) : false + const selectedIsSavingInstructions = selectedAgent + ? Boolean(savingInstructions[selectedAgent.agent_type]) + : false const selectedAgentKind = selectedAgent?.agent_type ?? null const selectedModelProviders = useMemo(() => { @@ -8822,6 +8873,63 @@ export function AcpAgentSettings() { + {selectedAgent.system_instructions_file_path && ( +
+
+ + + {selectedAgent.system_instructions_file_path} + +
+

+ {t("systemInstructions.hint")} +

+
+