diff --git a/src-tauri/src/deeplink/parser.rs b/src-tauri/src/deeplink/parser.rs index 943e576fa..32e4f4223 100644 --- a/src-tauri/src/deeplink/parser.rs +++ b/src-tauri/src/deeplink/parser.rs @@ -91,9 +91,9 @@ fn parse_provider_deeplink( // Validate app type. // // Oh My Pi is refused with the same guidance as the import path - // (`deeplink/provider.rs`): its providers are created from the Oh My Pi - // page, where the models.yml text editor can compose the entry. Accepting - // the link here would only fail later with a generic "invalid app" error. + // (`deeplink/provider.rs`): a models.yml entry needs its models listed, which + // the Oh My Pi provider form composes. Accepting the link here would only + // fail later with a generic "invalid app" error. if matches!(app.as_str(), "omp" | "ohmypi" | "oh-my-pi" | "oh_my_pi") { return Err(AppError::InvalidInput( "Oh My Pi providers must be added from the Oh My Pi provider page".to_string(), @@ -253,13 +253,7 @@ fn parse_prompt_deeplink( .ok_or_else(|| AppError::InvalidInput("Missing 'app' parameter for prompt".to_string()))? .clone(); - // Validate app type. - // - // Oh My Pi is deliberately absent: it has no prompt panel, so accepting a - // link for it would let an import write `/AGENTS.md` through - // `PromptService`, with no surface in the app showing what landed there and - // no way to undo it. The prompt file mapping stays in place for the day a - // panel exists; until then the link is refused at parse time. + // Validate app type if !matches!( app.as_str(), "claude" diff --git a/src-tauri/src/deeplink/tests.rs b/src-tauri/src/deeplink/tests.rs index 50b57686c..2cf46d6e5 100644 --- a/src-tauri/src/deeplink/tests.rs +++ b/src-tauri/src/deeplink/tests.rs @@ -233,7 +233,7 @@ fn test_parse_mcp_deeplink_accepts_pi_mcode_and_omp() { /// 供应商链接对 Oh My Pi 明确拒绝,而不是落到「Invalid provider app type」。 #[test] fn test_parse_provider_deeplink_refuses_omp_with_guidance() { - for app in ["omp", "omp", "oh-my-pi", "oh_my_pi"] { + for app in ["omp", "ohmypi", "oh-my-pi", "oh_my_pi"] { let url = format!("ccswitch://v1/import?resource=provider&app={app}&name=Test&endpoint=https%3A%2F%2Fapi.example.com"); let err = parse_deeplink_url(&url).unwrap_err().to_string(); @@ -252,15 +252,6 @@ fn test_parse_prompt_deeplink_accepts_mcode() { assert_eq!(request.app.as_deref(), Some("mcode")); } -/// Oh My Pi has no prompt panel: the link must be refused instead of writing -/// `~/.omp/agent/AGENTS.md` with nothing in the UI showing what landed there. -#[test] -fn test_parse_prompt_deeplink_refuses_omp() { - let url = "ccswitch://v1/import?resource=prompt&app=omp&name=Rules&content=IyBoaQ%3D%3D"; - let err = parse_deeplink_url(url).unwrap_err().to_string(); - assert!(err.contains("Invalid app type"), "{err}"); -} - /// 读取不清除,按编号确认才清除:前端读到后、显示前被打断(effect 重建、窗口销毁), /// 下次读取仍能拿到;期间来了新链接,迟到的旧确认不能把新链接清掉 #[test] diff --git a/src-tauri/src/prompt_files.rs b/src-tauri/src/prompt_files.rs index 65ba6316d..1ea046573 100644 --- a/src-tauri/src/prompt_files.rs +++ b/src-tauri/src/prompt_files.rs @@ -19,7 +19,7 @@ pub(crate) fn validate_prompt_content(app: &AppType, content: &str) -> Result<() /// 返回指定应用所使用的提示词文件路径。 pub fn prompt_file_path(app: &AppType) -> Result { - if matches!(app, AppType::ClaudeDesktop | AppType::Omp) { + if matches!(app, AppType::ClaudeDesktop) { return Err(AppError::localized( "app.prompts_unsupported", "当前应用暂不支持 Prompts", @@ -41,7 +41,9 @@ pub fn prompt_file_path(app: &AppType) -> Result { // agy 只从各「定制根目录」读独立的 AGENTS.md;全局那一个是 ~/.gemini/config,Key 模式的 // 私有 --gemini_dir 也经软链共享它。别用 ~/.gemini/GEMINI.md:那是 Gemini CLI 的。 AppType::Agy => crate::agy_config::customizations_dir(), - AppType::ClaudeDesktop | AppType::Omp => unreachable!("handled above"), + // omp 只读按 profile 切换的 agent 目录里的 AGENTS.md(上游 discovery/builtin.ts) + AppType::Omp => crate::omp_config::get_omp_agent_dir()?, + AppType::ClaudeDesktop => unreachable!("handled above"), }; let filename = match app { @@ -50,9 +52,8 @@ pub fn prompt_file_path(app: &AppType) -> Result { AppType::Gemini => "GEMINI.md", AppType::GrokBuild | AppType::OpenCode | AppType::OpenClaw => "AGENTS.md", AppType::Hermes => "SOUL.md", - AppType::Pi | AppType::Mcode | AppType::Agy | AppType::Kimi => "AGENTS.md", - - AppType::ClaudeDesktop | AppType::Omp => unreachable!("handled above"), + AppType::Pi | AppType::Mcode | AppType::Agy | AppType::Kimi | AppType::Omp => "AGENTS.md", + AppType::ClaudeDesktop => unreachable!("handled above"), }; Ok(base_dir.join(filename)) diff --git a/src-tauri/src/services/prompt.rs b/src-tauri/src/services/prompt.rs index b84f1c05d..1f0f84d51 100644 --- a/src-tauri/src/services/prompt.rs +++ b/src-tauri/src/services/prompt.rs @@ -281,7 +281,7 @@ impl PromptService { pub fn sync_to_live(state: &AppState, app: AppType) -> Result<(), AppError> { // Pi derives activation from its native AGENTS.md; its persisted prompt // rows are intentionally disabled and must not drive generic projection. - if matches!(app, AppType::ClaudeDesktop | AppType::Pi | AppType::Omp) { + if matches!(app, AppType::ClaudeDesktop | AppType::Pi) { return Ok(()); } diff --git a/src-tauri/tests/prompt_live_sync.rs b/src-tauri/tests/prompt_live_sync.rs index 763601f24..baa62d3d0 100644 --- a/src-tauri/tests/prompt_live_sync.rs +++ b/src-tauri/tests/prompt_live_sync.rs @@ -193,3 +193,35 @@ fn saving_inactive_prompts_keeps_a_hand_written_file_until_the_last_enabled_one_ update_settings(AppSettings::default()).unwrap(); let _ = fs::remove_dir_all(&hermes_dir); } + +#[test] +fn omp_prompts_write_agents_md_in_the_agent_directory() { + let _guard = test_mutex().lock().unwrap(); + reset_test_fs(); + let home = ensure_test_home(); + let agent_dir = home.join("custom-omp/agent"); + update_settings(AppSettings { + omp_config_dir: Some(agent_dir.to_string_lossy().into_owned()), + ..AppSettings::default() + }) + .unwrap(); + let state = create_test_state().unwrap(); + + let prompt = Prompt { + id: "global".into(), + name: "Global".into(), + content: "Use the project conventions.".into(), + description: None, + enabled: false, + created_at: None, + updated_at: None, + }; + PromptService::upsert_prompt(&state, AppType::Omp, "global", prompt).unwrap(); + PromptService::enable_prompt(&state, AppType::Omp, "global").unwrap(); + assert_eq!( + fs::read_to_string(agent_dir.join("AGENTS.md")).unwrap(), + "Use the project conventions." + ); + + update_settings(AppSettings::default()).unwrap(); +} diff --git a/src/components/prompts/promptUtils.tsx b/src/components/prompts/promptUtils.tsx index 9ada74518..2604c5a56 100644 --- a/src/components/prompts/promptUtils.tsx +++ b/src/components/prompts/promptUtils.tsx @@ -22,6 +22,7 @@ const PROMPT_FILE_NAME: Partial> = { pi: "AGENTS.md", mcode: "AGENTS.md", kimi: "AGENTS.md", + omp: "AGENTS.md", }; export function promptFileName(app: AppId): string { diff --git a/src/lib/query/prompts.ts b/src/lib/query/prompts.ts index dc806410b..829db9898 100644 --- a/src/lib/query/prompts.ts +++ b/src/lib/query/prompts.ts @@ -23,6 +23,7 @@ export const PROMPT_APP_IDS: AppId[] = [ "mcode", "kimi", "agy", + "omp", ]; /** 应用页对应的提示词应用;不支持提示词的返回 null。 */