feat(omo): add OMO Slim (oh-my-opencode-slim) support

Implement full OMO Slim profile management to align with ai-toolbox:
- Backend: Slim service methods, DAO, Tauri commands, plugin conflict handling
- Frontend: types, API, query hooks, form integration with isSlim parameterization
- Slim variant: 6 agents (no categories), separate config file and plugin name
- Mutual exclusion: standard OMO and Slim cannot coexist as plugins
- i18n: zh/en/ja translations for all Slim agent descriptions
This commit is contained in:
Jason
2026-02-19 20:47:55 +08:00
parent 51476953ae
commit 8e219b5eb1
26 changed files with 1176 additions and 165 deletions
+11 -1
View File
@@ -215,7 +215,7 @@ pub async fn set_common_config_snippet(
) -> Result<(), String> {
if !snippet.trim().is_empty() {
match app_type.as_str() {
"claude" | "gemini" | "omo" => {
"claude" | "gemini" | "omo" | "omo-slim" => {
serde_json::from_str::<serde_json::Value>(&snippet)
.map_err(invalid_json_format_error)?;
}
@@ -245,6 +245,16 @@ pub async fn set_common_config_snippet(
crate::services::OmoService::write_config_to_file(state.inner())
.map_err(|e| e.to_string())?;
}
if app_type == "omo-slim"
&& state
.db
.get_current_omo_slim_provider("opencode")
.map_err(|e| e.to_string())?
.is_some()
{
crate::services::OmoService::write_config_to_file_slim(state.inner())
.map_err(|e| e.to_string())?;
}
Ok(())
}
+49
View File
@@ -48,3 +48,52 @@ pub async fn get_omo_provider_count(state: State<'_, AppState>) -> Result<usize,
.count();
Ok(count)
}
// ── OMO Slim commands ───────────────────────────────────────
#[tauri::command]
pub async fn read_omo_slim_local_file() -> Result<OmoLocalFileData, String> {
OmoService::read_local_file_slim().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn get_current_omo_slim_provider_id(
state: State<'_, AppState>,
) -> Result<String, String> {
let provider = state
.db
.get_current_omo_slim_provider("opencode")
.map_err(|e| e.to_string())?;
Ok(provider.map(|p| p.id).unwrap_or_default())
}
#[tauri::command]
pub async fn disable_current_omo_slim(state: State<'_, AppState>) -> Result<(), String> {
let providers = state
.db
.get_all_providers("opencode")
.map_err(|e| e.to_string())?;
for (id, p) in &providers {
if p.category.as_deref() == Some("omo-slim") {
state
.db
.clear_omo_slim_provider_current("opencode", id)
.map_err(|e| e.to_string())?;
}
}
OmoService::delete_config_file_slim().map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub async fn get_omo_slim_provider_count(state: State<'_, AppState>) -> Result<usize, String> {
let providers = state
.db
.get_all_providers("opencode")
.map_err(|e| e.to_string())?;
let count = providers
.values()
.filter(|p| p.category.as_deref() == Some("omo-slim"))
.count();
Ok(count)
}
+19
View File
@@ -70,4 +70,23 @@ impl Database {
self.set_setting("common_config_omo", &json_str)?;
Ok(())
}
// ── OMO Slim global config ──────────────────────────────────
pub fn get_omo_slim_global_config(&self) -> Result<OmoGlobalConfig, AppError> {
let json_str = self.get_setting("common_config_omo_slim")?;
match json_str {
Some(s) => serde_json::from_str::<OmoGlobalConfig>(&s).map_err(|e| {
AppError::Config(format!("Failed to parse common_config_omo_slim: {e}"))
}),
None => Ok(OmoGlobalConfig::default()),
}
}
pub fn save_omo_slim_global_config(&self, config: &OmoGlobalConfig) -> Result<(), AppError> {
let json_str = serde_json::to_string(config)
.map_err(|e| AppError::Config(format!("JSON serialization failed: {e}")))?;
self.set_setting("common_config_omo_slim", &json_str)?;
Ok(())
}
}
+127
View File
@@ -481,4 +481,131 @@ impl Database {
in_failover_queue: false,
}))
}
// ── OMO Slim provider management ────────────────────────────
pub fn set_omo_slim_provider_current(
&self,
app_type: &str,
provider_id: &str,
) -> Result<(), AppError> {
let mut conn = lock_conn!(self.conn);
let tx = conn
.transaction()
.map_err(|e| AppError::Database(e.to_string()))?;
tx.execute(
"UPDATE providers SET is_current = 0 WHERE app_type = ?1 AND category = 'omo-slim'",
params![app_type],
)
.map_err(|e| AppError::Database(e.to_string()))?;
let updated = tx
.execute(
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2 AND category = 'omo-slim'",
params![provider_id, app_type],
)
.map_err(|e| AppError::Database(e.to_string()))?;
if updated != 1 {
return Err(AppError::Database(format!(
"Failed to set OMO Slim provider current: provider '{provider_id}' not found in app '{app_type}'"
)));
}
tx.commit().map_err(|e| AppError::Database(e.to_string()))?;
Ok(())
}
pub fn is_omo_slim_provider_current(
&self,
app_type: &str,
provider_id: &str,
) -> Result<bool, AppError> {
let conn = lock_conn!(self.conn);
match conn.query_row(
"SELECT is_current FROM providers
WHERE id = ?1 AND app_type = ?2 AND category = 'omo-slim'",
params![provider_id, app_type],
|row| row.get(0),
) {
Ok(is_current) => Ok(is_current),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
Err(e) => Err(AppError::Database(e.to_string())),
}
}
pub fn clear_omo_slim_provider_current(
&self,
app_type: &str,
provider_id: &str,
) -> Result<(), AppError> {
let conn = lock_conn!(self.conn);
conn.execute(
"UPDATE providers SET is_current = 0
WHERE id = ?1 AND app_type = ?2 AND category = 'omo-slim'",
params![provider_id, app_type],
)
.map_err(|e| AppError::Database(e.to_string()))?;
Ok(())
}
pub fn get_current_omo_slim_provider(
&self,
app_type: &str,
) -> Result<Option<Provider>, AppError> {
let conn = lock_conn!(self.conn);
let row_data: Result<OmoProviderRow, rusqlite::Error> = conn.query_row(
"SELECT id, name, settings_config, category, created_at, sort_index, notes, meta
FROM providers
WHERE app_type = ?1 AND category = 'omo-slim' AND is_current = 1
LIMIT 1",
params![app_type],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
row.get(5)?,
row.get(6)?,
row.get(7)?,
))
},
);
let (id, name, settings_config_str, category, created_at, sort_index, notes, meta_str) =
match row_data {
Ok(v) => v,
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
Err(e) => return Err(AppError::Database(e.to_string())),
};
let settings_config = serde_json::from_str(&settings_config_str).map_err(|e| {
AppError::Database(format!(
"Failed to parse OMO Slim provider settings_config (provider_id={id}): {e}"
))
})?;
let meta: crate::provider::ProviderMeta = if meta_str.trim().is_empty() {
crate::provider::ProviderMeta::default()
} else {
serde_json::from_str(&meta_str).map_err(|e| {
AppError::Database(format!(
"Failed to parse OMO Slim provider meta (provider_id={id}): {e}"
))
})?
};
Ok(Some(Provider {
id,
name,
settings_config,
website_url: None,
category,
created_at,
sort_index,
notes,
meta: Some(meta),
icon: None,
icon_color: None,
in_failover_queue: false,
}))
}
}
+34 -1
View File
@@ -526,7 +526,36 @@ pub fn run() {
}
}
// 2.3 OpenClaw 供应商导入(累加式模式,需特殊处理)
// 2.3 OMO Slim config import (when no omo-slim provider in DB, import from local)
{
let has_omo_slim = app_state
.db
.get_all_providers("opencode")
.map(|providers| {
providers
.values()
.any(|p| p.category.as_deref() == Some("omo-slim"))
})
.unwrap_or(false);
if !has_omo_slim {
match crate::services::OmoService::import_from_local_slim(&app_state) {
Ok(provider) => {
log::info!(
"✓ Imported OMO Slim config from local as provider '{}'",
provider.name
);
}
Err(AppError::OmoConfigNotFound) => {
log::debug!("○ No OMO Slim config to import");
}
Err(e) => {
log::warn!("✗ Failed to import OMO Slim config from local: {e}");
}
}
}
}
// 2.4 OpenClaw 供应商导入(累加式模式,需特殊处理)
// OpenClaw 与 OpenCode 类似:配置文件中可同时存在多个供应商
// 需要遍历 models.providers 字段下的每个供应商并导入
match crate::services::provider::import_openclaw_providers_from_live(&app_state) {
@@ -1035,6 +1064,10 @@ pub fn run() {
commands::get_current_omo_provider_id,
commands::get_omo_provider_count,
commands::disable_current_omo,
commands::read_omo_slim_local_file,
commands::get_current_omo_slim_provider_id,
commands::get_omo_slim_provider_count,
commands::disable_current_omo_slim,
// Workspace files (OpenClaw)
commands::read_workspace_file,
commands::write_workspace_file,
+11
View File
@@ -145,14 +145,25 @@ pub fn add_plugin(plugin_name: &str) -> Result<(), AppError> {
match plugins {
Some(arr) => {
// Mutual exclusion: standard OMO and OMO Slim cannot coexist as plugins
if plugin_name.starts_with("oh-my-opencode")
&& !plugin_name.starts_with("oh-my-opencode-slim")
{
// Adding standard OMO -> remove all Slim variants
arr.retain(|v| {
v.as_str()
.map(|s| !s.starts_with("oh-my-opencode-slim"))
.unwrap_or(true)
});
} else if plugin_name.starts_with("oh-my-opencode-slim") {
// Adding Slim -> remove all standard OMO variants (but keep slim)
arr.retain(|v| {
v.as_str()
.map(|s| {
!s.starts_with("oh-my-opencode") || s.starts_with("oh-my-opencode-slim")
})
.unwrap_or(true)
});
}
let already_exists = arr.iter().any(|v| v.as_str() == Some(plugin_name));
+224 -17
View File
@@ -52,26 +52,48 @@ impl OmoService {
.ok_or_else(|| AppError::Config("Expected JSON object".to_string()))
}
fn extract_other_fields(obj: &Map<String, Value>) -> Map<String, Value> {
const KNOWN_KEYS: [&str; 13] = [
"$schema",
"agents",
"categories",
"sisyphus_agent",
"disabled_agents",
"disabled_mcps",
"disabled_hooks",
"disabled_skills",
"lsp",
"experimental",
"background_task",
"browser_automation_engine",
"claude_code",
];
const KNOWN_KEYS: [&str; 13] = [
"$schema",
"agents",
"categories",
"sisyphus_agent",
"disabled_agents",
"disabled_mcps",
"disabled_hooks",
"disabled_skills",
"lsp",
"experimental",
"background_task",
"browser_automation_engine",
"claude_code",
];
const KNOWN_KEYS_SLIM: [&str; 8] = [
"$schema",
"agents",
"sisyphus_agent",
"disabled_agents",
"disabled_mcps",
"disabled_hooks",
"lsp",
"experimental",
];
fn extract_other_fields(obj: &Map<String, Value>) -> Map<String, Value> {
Self::extract_other_fields_with_keys(obj, &Self::KNOWN_KEYS)
}
fn extract_other_fields_slim(obj: &Map<String, Value>) -> Map<String, Value> {
Self::extract_other_fields_with_keys(obj, &Self::KNOWN_KEYS_SLIM)
}
fn extract_other_fields_with_keys(
obj: &Map<String, Value>,
known: &[&str],
) -> Map<String, Value> {
let mut other = Map::new();
for (k, v) in obj {
if !KNOWN_KEYS.contains(&k.as_str()) {
if !known.contains(&k.as_str()) {
other.insert(k.clone(), v.clone());
}
}
@@ -142,6 +164,24 @@ impl OmoService {
}
}
fn config_path_slim() -> PathBuf {
get_opencode_dir().join("oh-my-opencode-slim.jsonc")
}
fn resolve_local_config_path_slim() -> Result<PathBuf, AppError> {
let config_path = Self::config_path_slim();
if config_path.exists() {
return Ok(config_path);
}
let json_path = config_path.with_extension("json");
if json_path.exists() {
return Ok(json_path);
}
Err(AppError::OmoConfigNotFound)
}
pub fn delete_config_file() -> Result<(), AppError> {
let config_path = Self::config_path();
if config_path.exists() {
@@ -152,6 +192,16 @@ impl OmoService {
Ok(())
}
pub fn delete_config_file_slim() -> Result<(), AppError> {
let config_path = Self::config_path_slim();
if config_path.exists() {
std::fs::remove_file(&config_path).map_err(|e| AppError::io(&config_path, e))?;
log::info!("OMO Slim config file deleted: {config_path:?}");
}
crate::opencode_config::remove_plugin_by_prefix("oh-my-opencode-slim")?;
Ok(())
}
pub fn write_config_to_file(state: &AppState) -> Result<(), AppError> {
let global = state.db.get_omo_global_config()?;
let current_omo = state.db.get_current_omo_provider("opencode")?;
@@ -183,6 +233,36 @@ impl OmoService {
Ok(())
}
pub fn write_config_to_file_slim(state: &AppState) -> Result<(), AppError> {
let global = state.db.get_omo_slim_global_config()?;
let current_omo = state.db.get_current_omo_slim_provider("opencode")?;
let profile_data = current_omo.as_ref().map(|p| {
let agents = p.settings_config.get("agents").cloned();
let other_fields = p.settings_config.get("otherFields").cloned();
let use_common_config = p
.settings_config
.get("useCommonConfig")
.and_then(|v| v.as_bool())
.unwrap_or(true);
(agents, None, other_fields, use_common_config)
});
let merged = Self::merge_config_slim(&global, profile_data.as_ref());
let config_path = Self::config_path_slim();
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
write_json_file(&config_path, &merged)?;
crate::opencode_config::add_plugin("oh-my-opencode-slim@latest")?;
log::info!("OMO Slim config written to {config_path:?}");
Ok(())
}
fn merge_config(global: &OmoGlobalConfig, profile_data: Option<&OmoProfileData>) -> Value {
let mut result = Map::new();
let use_common_config = profile_data.map(|(_, _, _, v)| *v).unwrap_or(true);
@@ -219,6 +299,36 @@ impl OmoService {
Value::Object(result)
}
/// Merge config for Slim variant (no categories, no disabled_skills, no background_task, etc.)
fn merge_config_slim(global: &OmoGlobalConfig, profile_data: Option<&OmoProfileData>) -> Value {
let mut result = Map::new();
let use_common_config = profile_data.map(|(_, _, _, v)| *v).unwrap_or(true);
if use_common_config {
if let Some(url) = &global.schema_url {
result.insert("$schema".to_string(), Value::String(url.clone()));
}
Self::insert_opt_value(&mut result, "sisyphus_agent", &global.sisyphus_agent);
Self::insert_string_array(&mut result, "disabled_agents", &global.disabled_agents);
Self::insert_string_array(&mut result, "disabled_mcps", &global.disabled_mcps);
Self::insert_string_array(&mut result, "disabled_hooks", &global.disabled_hooks);
// Slim does NOT support: disabled_skills, background_task, browser_automation_engine, claude_code
Self::insert_opt_value(&mut result, "lsp", &global.lsp);
Self::insert_opt_value(&mut result, "experimental", &global.experimental);
Self::insert_object_entries(&mut result, global.other_fields.as_ref());
}
if let Some((agents, _categories, other_fields, _)) = profile_data {
Self::insert_opt_value(&mut result, "agents", agents);
// Slim does NOT have categories
Self::insert_object_entries(&mut result, other_fields.as_ref());
}
Value::Object(result)
}
pub fn import_from_local(state: &AppState) -> Result<crate::provider::Provider, AppError> {
let actual_path = Self::resolve_local_config_path()?;
Self::import_from_path(state, &actual_path)
@@ -277,6 +387,58 @@ impl OmoService {
Ok(provider)
}
pub fn import_from_local_slim(state: &AppState) -> Result<crate::provider::Provider, AppError> {
let actual_path = Self::resolve_local_config_path_slim()?;
let obj = Self::read_jsonc_object(&actual_path)?;
let mut settings = Map::new();
if let Some(agents) = obj.get("agents") {
settings.insert("agents".to_string(), agents.clone());
}
// Slim has no categories
settings.insert("useCommonConfig".to_string(), Value::Bool(true));
let other = Self::extract_other_fields_slim(&obj);
if !other.is_empty() {
settings.insert("otherFields".to_string(), Value::Object(other));
}
let mut global = state.db.get_omo_slim_global_config()?;
Self::merge_global_from_obj(&obj, &mut global);
global.updated_at = chrono::Utc::now().to_rfc3339();
state.db.save_omo_slim_global_config(&global)?;
let provider_id = format!("omo-slim-{}", uuid::Uuid::new_v4());
let name = format!(
"Imported Slim {}",
chrono::Local::now().format("%Y-%m-%d %H:%M")
);
let settings_config =
serde_json::to_value(&settings).unwrap_or_else(|_| serde_json::json!({}));
let provider = crate::provider::Provider {
id: provider_id,
name,
settings_config,
website_url: None,
category: Some("omo-slim".to_string()),
created_at: Some(chrono::Utc::now().timestamp_millis()),
sort_index: None,
notes: None,
meta: None,
icon: None,
icon_color: None,
in_failover_queue: false,
};
state.db.save_provider("opencode", &provider)?;
state
.db
.set_omo_slim_provider_current("opencode", &provider.id)?;
Self::write_config_to_file_slim(state)?;
Ok(provider)
}
pub fn read_local_file() -> Result<OmoLocalFileData, AppError> {
let actual_path = Self::resolve_local_config_path()?;
let metadata = std::fs::metadata(&actual_path).ok();
@@ -293,6 +455,22 @@ impl OmoService {
))
}
pub fn read_local_file_slim() -> Result<OmoLocalFileData, AppError> {
let actual_path = Self::resolve_local_config_path_slim()?;
let metadata = std::fs::metadata(&actual_path).ok();
let last_modified = metadata
.and_then(|m| m.modified().ok())
.map(|t| chrono::DateTime::<chrono::Utc>::from(t).to_rfc3339());
let obj = Self::read_jsonc_object(&actual_path)?;
Ok(Self::build_local_file_data_slim_from_obj(
&obj,
actual_path.to_string_lossy().to_string(),
last_modified,
))
}
fn build_local_file_data_from_obj(
obj: &Map<String, Value>,
file_path: String,
@@ -322,6 +500,35 @@ impl OmoService {
}
}
fn build_local_file_data_slim_from_obj(
obj: &Map<String, Value>,
file_path: String,
last_modified: Option<String>,
) -> OmoLocalFileData {
let agents = obj.get("agents").cloned();
// Slim has no categories
let other = Self::extract_other_fields_slim(obj);
let other_fields = if other.is_empty() {
None
} else {
Some(Value::Object(other))
};
let mut global = OmoGlobalConfig::default();
Self::merge_global_from_obj(obj, &mut global);
global.other_fields = other_fields.clone();
OmoLocalFileData {
agents,
categories: None,
other_fields,
global,
file_path,
last_modified,
}
}
fn strip_jsonc_comments(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
+72 -14
View File
@@ -167,8 +167,7 @@ impl ProviderService {
// Additive mode apps (OpenCode, OpenClaw) - always write to live config
if app_type.is_additive_mode() {
// OMO providers use exclusive mode and write to dedicated config file.
if matches!(app_type, AppType::OpenCode)
&& provider.category.as_deref() == Some("omo")
if matches!(app_type, AppType::OpenCode) && provider.category.as_deref() == Some("omo")
{
// Do not auto-enable newly added OMO providers.
// Users must explicitly switch/apply an OMO provider to activate it.
@@ -207,8 +206,7 @@ impl ProviderService {
// Additive mode apps (OpenCode, OpenClaw) - always update in live config
if app_type.is_additive_mode() {
if matches!(app_type, AppType::OpenCode)
&& provider.category.as_deref() == Some("omo")
if matches!(app_type, AppType::OpenCode) && provider.category.as_deref() == Some("omo")
{
let is_omo_current = state
.db
@@ -218,6 +216,17 @@ impl ProviderService {
}
return Ok(true);
}
if matches!(app_type, AppType::OpenCode)
&& provider.category.as_deref() == Some("omo-slim")
{
let is_current = state
.db
.is_omo_slim_provider_current(app_type.as_str(), &provider.id)?;
if is_current {
crate::services::OmoService::write_config_to_file_slim(state)?;
}
return Ok(true);
}
write_live_snapshot(&app_type, &provider)?;
return Ok(true);
}
@@ -264,14 +273,12 @@ impl ProviderService {
// Additive mode apps - no current provider concept
if app_type.is_additive_mode() {
if matches!(app_type, AppType::OpenCode) {
let is_omo = state
let provider_category = state
.db
.get_provider_by_id(id, app_type.as_str())?
.and_then(|p| p.category)
.as_deref()
== Some("omo");
.and_then(|p| p.category);
if is_omo {
if provider_category.as_deref() == Some("omo") {
let was_current = state.db.is_omo_provider_current(app_type.as_str(), id)?;
let omo_count = state
.db
@@ -292,6 +299,30 @@ impl ProviderService {
}
return Ok(());
}
if provider_category.as_deref() == Some("omo-slim") {
let was_current = state
.db
.is_omo_slim_provider_current(app_type.as_str(), id)?;
let slim_count = state
.db
.get_all_providers(app_type.as_str())?
.values()
.filter(|p| p.category.as_deref() == Some("omo-slim"))
.count();
if slim_count <= 1 && was_current {
return Err(AppError::Message(
"无法删除当前启用的最后一个 OMO Slim 配置,请先停用".to_string(),
));
}
state.db.delete_provider(app_type.as_str(), id)?;
if was_current {
crate::services::OmoService::delete_config_file_slim()?;
}
return Ok(());
}
}
// Remove from database
state.db.delete_provider(app_type.as_str(), id)?;
@@ -329,14 +360,12 @@ impl ProviderService {
) -> Result<(), AppError> {
match app_type {
AppType::OpenCode => {
let is_omo = state
let provider_category = state
.db
.get_provider_by_id(id, app_type.as_str())?
.and_then(|p| p.category)
.as_deref()
== Some("omo");
.and_then(|p| p.category);
if is_omo {
if provider_category.as_deref() == Some("omo") {
state.db.clear_omo_provider_current(app_type.as_str(), id)?;
let still_has_current =
state.db.get_current_omo_provider("opencode")?.is_some();
@@ -345,6 +374,19 @@ impl ProviderService {
} else {
crate::services::OmoService::delete_config_file()?;
}
} else if provider_category.as_deref() == Some("omo-slim") {
state
.db
.clear_omo_slim_provider_current(app_type.as_str(), id)?;
let still_has_current = state
.db
.get_current_omo_slim_provider("opencode")?
.is_some();
if still_has_current {
crate::services::OmoService::write_config_to_file_slim(state)?;
} else {
crate::services::OmoService::delete_config_file_slim()?;
}
} else {
remove_opencode_provider_from_live(id)?;
}
@@ -386,6 +428,13 @@ impl ProviderService {
return Self::switch_normal(state, app_type, id, &providers);
}
// OMO Slim providers are switched through their own exclusive path.
if matches!(app_type, AppType::OpenCode)
&& _provider.category.as_deref() == Some("omo-slim")
{
return Self::switch_normal(state, app_type, id, &providers);
}
// Check if proxy takeover mode is active AND proxy server is actually running
// Both conditions must be true to use hot-switch mode
// Use blocking wait since this is a sync function
@@ -463,6 +512,15 @@ impl ProviderService {
return Ok(());
}
if matches!(app_type, AppType::OpenCode) && provider.category.as_deref() == Some("omo-slim")
{
state
.db
.set_omo_slim_provider_current(app_type.as_str(), id)?;
crate::services::OmoService::write_config_to_file_slim(state)?;
return Ok(());
}
// Backfill: Backfill current live config to current provider
// Use effective current provider (validated existence) to ensure backfill targets valid provider
let current_id = crate::settings::get_effective_current_provider(&state.db, &app_type)?;
+26 -1
View File
@@ -61,7 +61,10 @@ import { UniversalProviderPanel } from "@/components/universal";
import { McpIcon } from "@/components/BrandIcons";
import { Button } from "@/components/ui/button";
import { SessionManagerPage } from "@/components/sessions/SessionManagerPage";
import { useDisableCurrentOmo } from "@/lib/query/omo";
import {
useDisableCurrentOmo,
useDisableCurrentOmoSlim,
} from "@/lib/query/omo";
import WorkspaceFilesPanel from "@/components/workspace/WorkspaceFilesPanel";
import EnvPanel from "@/components/openclaw/EnvPanel";
import ToolsPanel from "@/components/openclaw/ToolsPanel";
@@ -248,6 +251,23 @@ function App() {
});
};
const disableOmoSlimMutation = useDisableCurrentOmoSlim();
const handleDisableOmoSlim = () => {
disableOmoSlimMutation.mutate(undefined, {
onSuccess: () => {
toast.success(t("omo.disabled", { defaultValue: "OMO 已停用" }));
},
onError: (error: Error) => {
toast.error(
t("omo.disableFailed", {
defaultValue: "停用 OMO 失败: {{error}}",
error: extractErrorMessage(error),
}),
);
},
});
};
useEffect(() => {
let unsubscribe: (() => void) | undefined;
@@ -746,6 +766,11 @@ function App() {
onDisableOmo={
activeApp === "opencode" ? handleDisableOmo : undefined
}
onDisableOmoSlim={
activeApp === "opencode"
? handleDisableOmoSlim
: undefined
}
onDuplicate={handleDuplicateProvider}
onConfigureUsage={setUsageProvider}
onOpenWebsite={handleOpenWebsite}
+25 -8
View File
@@ -29,11 +29,14 @@ interface ProviderCardProps {
isInConfig?: boolean; // OpenCode: 是否已添加到 opencode.json
isOmo?: boolean;
isLastOmo?: boolean;
isOmoSlim?: boolean;
isLastOmoSlim?: boolean;
onSwitch: (provider: Provider) => void;
onEdit: (provider: Provider) => void;
onDelete: (provider: Provider) => void;
onRemoveFromConfig?: (provider: Provider) => void;
onDisableOmo?: () => void;
onDisableOmoSlim?: () => void;
onConfigureUsage: (provider: Provider) => void;
onOpenWebsite: (url: string) => void;
onDuplicate: (provider: Provider) => void;
@@ -92,11 +95,14 @@ export function ProviderCard({
isInConfig = true,
isOmo = false,
isLastOmo = false,
isOmoSlim = false,
isLastOmoSlim = false,
onSwitch,
onEdit,
onDelete,
onRemoveFromConfig,
onDisableOmo,
onDisableOmoSlim,
onConfigureUsage,
onOpenWebsite,
onDuplicate,
@@ -117,6 +123,11 @@ export function ProviderCard({
}: ProviderCardProps) {
const { t } = useTranslation();
// OMO and OMO Slim share the same card behavior
const isAnyOmo = isOmo || isOmoSlim;
const isLastAnyOmo = isOmo ? isLastOmo : isLastOmoSlim;
const handleDisableAnyOmo = isOmoSlim ? onDisableOmoSlim : onDisableOmo;
const { data: health } = useProviderHealth(provider.id, appId);
const fallbackUrlText = t("provider.notConfigured", {
@@ -186,11 +197,11 @@ export function ProviderCard({
};
// 判断是否是"当前使用中"的供应商
// - OMO 供应商:使用 isCurrent
// - OMO/OMO Slim 供应商:使用 isCurrent
// - 累加模式应用(OpenCode 非 OMO / OpenClaw):不存在"当前"概念,始终返回 false
// - 故障转移模式:代理实际使用的供应商(activeProviderId
// - 普通模式:isCurrent
const isActiveProvider = isOmo
const isActiveProvider = isAnyOmo
? isCurrent
: appId === "opencode" || appId === "openclaw"
? false
@@ -198,10 +209,10 @@ export function ProviderCard({
? activeProviderId === provider.id
: isCurrent;
const shouldUseGreen = !isOmo && isProxyTakeover && isActiveProvider;
const shouldUseGreen = !isAnyOmo && isProxyTakeover && isActiveProvider;
const shouldUseBlue =
(isOmo && isActiveProvider) ||
(!isOmo && !isProxyTakeover && isActiveProvider);
(isAnyOmo && isActiveProvider) ||
(!isAnyOmo && !isProxyTakeover && isActiveProvider);
return (
<div
@@ -265,6 +276,12 @@ export function ProviderCard({
</span>
)}
{isOmoSlim && (
<span className="inline-flex items-center rounded-md bg-indigo-100 px-1.5 py-0.5 text-[10px] font-semibold text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300">
Slim
</span>
)}
{isProxyRunning && isInFailoverQueue && health && (
<ProviderHealthBadge
consecutiveFailures={health.consecutive_failures}
@@ -372,8 +389,8 @@ export function ProviderCard({
isInConfig={isInConfig}
isTesting={isTesting}
isProxyTakeover={isProxyTakeover}
isOmo={isOmo}
isLastOmo={isLastOmo}
isOmo={isAnyOmo}
isLastOmo={isLastAnyOmo}
onSwitch={() => onSwitch(provider)}
onEdit={() => onEdit(provider)}
onDuplicate={() => onDuplicate(provider)}
@@ -385,7 +402,7 @@ export function ProviderCard({
? () => onRemoveFromConfig(provider)
: undefined
}
onDisableOmo={onDisableOmo}
onDisableOmo={handleDisableAnyOmo}
onOpenTerminal={
onOpenTerminal ? () => onOpenTerminal(provider) : undefined
}
+34 -2
View File
@@ -33,7 +33,12 @@ import {
useAddToFailoverQueue,
useRemoveFromFailoverQueue,
} from "@/lib/query/failover";
import { useCurrentOmoProviderId, useOmoProviderCount } from "@/lib/query/omo";
import {
useCurrentOmoProviderId,
useOmoProviderCount,
useCurrentOmoSlimProviderId,
useOmoSlimProviderCount,
} from "@/lib/query/omo";
import { useCallback } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@@ -47,6 +52,7 @@ interface ProviderListProps {
onDelete: (provider: Provider) => void;
onRemoveFromConfig?: (provider: Provider) => void;
onDisableOmo?: () => void;
onDisableOmoSlim?: () => void;
onDuplicate: (provider: Provider) => void;
onConfigureUsage?: (provider: Provider) => void;
onOpenWebsite: (url: string) => void;
@@ -68,6 +74,7 @@ export function ProviderList({
onDelete,
onRemoveFromConfig,
onDisableOmo,
onDisableOmoSlim,
onDuplicate,
onConfigureUsage,
onOpenWebsite,
@@ -135,6 +142,8 @@ export function ProviderList({
const isOpenCode = appId === "opencode";
const { data: currentOmoId } = useCurrentOmoProviderId(isOpenCode);
const { data: omoProviderCount } = useOmoProviderCount(isOpenCode);
const { data: currentOmoSlimId } = useCurrentOmoSlimProviderId(isOpenCode);
const { data: omoSlimProviderCount } = useOmoSlimProviderCount(isOpenCode);
const getFailoverPriority = useCallback(
(providerId: string): number | undefined => {
@@ -239,13 +248,20 @@ export function ProviderList({
<div className="space-y-3">
{filteredProviders.map((provider) => {
const isOmo = provider.category === "omo";
const isOmoSlim = provider.category === "omo-slim";
const isOmoCurrent = isOmo && provider.id === (currentOmoId || "");
const isOmoSlimCurrent =
isOmoSlim && provider.id === (currentOmoSlimId || "");
return (
<SortableProviderCard
key={provider.id}
provider={provider}
isCurrent={
isOmo ? isOmoCurrent : provider.id === currentProviderId
isOmo
? isOmoCurrent
: isOmoSlim
? isOmoSlimCurrent
: provider.id === currentProviderId
}
appId={appId}
isInConfig={isProviderInConfig(provider.id)}
@@ -253,11 +269,18 @@ export function ProviderList({
isLastOmo={
isOmo && (omoProviderCount ?? 0) <= 1 && isOmoCurrent
}
isOmoSlim={isOmoSlim}
isLastOmoSlim={
isOmoSlim &&
(omoSlimProviderCount ?? 0) <= 1 &&
isOmoSlimCurrent
}
onSwitch={onSwitch}
onEdit={onEdit}
onDelete={onDelete}
onRemoveFromConfig={onRemoveFromConfig}
onDisableOmo={onDisableOmo}
onDisableOmoSlim={onDisableOmoSlim}
onDuplicate={onDuplicate}
onConfigureUsage={onConfigureUsage}
onOpenWebsite={onOpenWebsite}
@@ -371,11 +394,14 @@ interface SortableProviderCardProps {
isInConfig: boolean;
isOmo: boolean;
isLastOmo: boolean;
isOmoSlim: boolean;
isLastOmoSlim: boolean;
onSwitch: (provider: Provider) => void;
onEdit: (provider: Provider) => void;
onDelete: (provider: Provider) => void;
onRemoveFromConfig?: (provider: Provider) => void;
onDisableOmo?: () => void;
onDisableOmoSlim?: () => void;
onDuplicate: (provider: Provider) => void;
onConfigureUsage?: (provider: Provider) => void;
onOpenWebsite: (url: string) => void;
@@ -401,11 +427,14 @@ function SortableProviderCard({
isInConfig,
isOmo,
isLastOmo,
isOmoSlim,
isLastOmoSlim,
onSwitch,
onEdit,
onDelete,
onRemoveFromConfig,
onDisableOmo,
onDisableOmoSlim,
onDuplicate,
onConfigureUsage,
onOpenWebsite,
@@ -445,11 +474,14 @@ function SortableProviderCard({
isInConfig={isInConfig}
isOmo={isOmo}
isLastOmo={isLastOmo}
isOmoSlim={isOmoSlim}
isLastOmoSlim={isLastOmoSlim}
onSwitch={onSwitch}
onEdit={onEdit}
onDelete={onDelete}
onRemoveFromConfig={onRemoveFromConfig}
onDisableOmo={onDisableOmo}
onDisableOmoSlim={onDisableOmoSlim}
onDuplicate={onDuplicate}
onConfigureUsage={
onConfigureUsage ? (item) => onConfigureUsage(item) : () => undefined
@@ -23,6 +23,7 @@ interface OmoCommonConfigEditorProps {
onGlobalConfigStateChange: (config: OmoGlobalConfig) => void;
globalConfigRef: React.RefObject<OmoGlobalConfigFieldsRef | null>;
fieldsKey: number;
isSlim?: boolean;
}
export function OmoCommonConfigEditor({
@@ -37,6 +38,7 @@ export function OmoCommonConfigEditor({
onGlobalConfigStateChange,
globalConfigRef,
fieldsKey,
isSlim = false,
}: OmoCommonConfigEditorProps) {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
@@ -153,6 +155,7 @@ export function OmoCommonConfigEditor({
ref={globalConfigRef as React.Ref<OmoGlobalConfigFieldsRef>}
onStateChange={onGlobalConfigStateChange}
hideSaveButtons
isSlim={isSlim}
/>
</div>
</FullScreenPanel>
@@ -41,10 +41,11 @@ import {
} from "lucide-react";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { useReadOmoLocalFile } from "@/lib/query/omo";
import { useReadOmoLocalFile, useReadOmoSlimLocalFile } from "@/lib/query/omo";
import {
OMO_BUILTIN_AGENTS,
OMO_BUILTIN_CATEGORIES,
OMO_SLIM_BUILTIN_AGENTS,
type OmoAgentDef,
type OmoCategoryDef,
} from "@/types/omo";
@@ -69,12 +70,13 @@ interface OmoFormFieldsProps {
>;
agents: Record<string, Record<string, unknown>>;
onAgentsChange: (agents: Record<string, Record<string, unknown>>) => void;
categories: Record<string, Record<string, unknown>>;
onCategoriesChange: (
categories?: Record<string, Record<string, unknown>>;
onCategoriesChange?: (
categories: Record<string, Record<string, unknown>>,
) => void;
otherFieldsStr: string;
onOtherFieldsStrChange: (value: string) => void;
isSlim?: boolean;
}
export type CustomModelItem = {
@@ -121,6 +123,9 @@ function DeferredKeyInput({
}
const BUILTIN_AGENT_KEYS = new Set(OMO_BUILTIN_AGENTS.map((a) => a.key));
const BUILTIN_AGENT_KEYS_SLIM = new Set(
OMO_SLIM_BUILTIN_AGENTS.map((a) => a.key),
);
const BUILTIN_CATEGORY_KEYS = new Set(OMO_BUILTIN_CATEGORIES.map((c) => c.key));
const EMPTY_VARIANT_VALUE = "__cc_switch_omo_variant_empty__";
@@ -303,13 +308,21 @@ export function OmoFormFields({
presetMetaMap: _presetMetaMap = {},
agents,
onAgentsChange,
categories,
categories = {},
onCategoriesChange,
otherFieldsStr,
onOtherFieldsStrChange,
isSlim = false,
}: OmoFormFieldsProps) {
const { t } = useTranslation();
const builtinAgentDefs = isSlim
? OMO_SLIM_BUILTIN_AGENTS
: OMO_BUILTIN_AGENTS;
const builtinAgentKeys = isSlim
? BUILTIN_AGENT_KEYS_SLIM
: BUILTIN_AGENT_KEYS;
const [mainAgentsOpen, setMainAgentsOpen] = useState(true);
const [subAgentsOpen, setSubAgentsOpen] = useState(true);
const [categoriesOpen, setCategoriesOpen] = useState(true);
@@ -329,7 +342,7 @@ export function OmoFormFields({
>({});
const [customAgents, setCustomAgents] = useState<CustomModelItem[]>(() =>
collectCustomModels(agents, BUILTIN_AGENT_KEYS),
collectCustomModels(agents, builtinAgentKeys),
);
const [customCategories, setCustomCategories] = useState<CustomModelItem[]>(
@@ -337,7 +350,7 @@ export function OmoFormFields({
);
useEffect(() => {
setCustomAgents(collectCustomModels(agents, BUILTIN_AGENT_KEYS));
setCustomAgents(collectCustomModels(agents, builtinAgentKeys));
}, [agents]);
useEffect(() => {
@@ -349,17 +362,18 @@ export function OmoFormFields({
onAgentsChange(
mergeCustomModelsIntoStore(
agents,
BUILTIN_AGENT_KEYS,
builtinAgentKeys,
customs,
modelVariantsMap,
),
);
},
[agents, onAgentsChange, modelVariantsMap],
[agents, onAgentsChange, modelVariantsMap, builtinAgentKeys],
);
const syncCustomCategories = useCallback(
(customs: CustomModelItem[]) => {
if (!onCategoriesChange) return;
onCategoriesChange(
mergeCustomModelsIntoStore(
categories,
@@ -709,7 +723,7 @@ export function OmoFormFields({
}
const updatedAgents = { ...agents };
for (const agentDef of OMO_BUILTIN_AGENTS) {
for (const agentDef of builtinAgentDefs) {
const recommendedValue = resolveRecommendedModel(agentDef.recommended);
if (recommendedValue && !updatedAgents[agentDef.key]?.model) {
updatedAgents[agentDef.key] = {
@@ -720,30 +734,35 @@ export function OmoFormFields({
}
onAgentsChange(updatedAgents);
const updatedCategories = { ...categories };
for (const catDef of OMO_BUILTIN_CATEGORIES) {
const recommendedValue = resolveRecommendedModel(catDef.recommended);
if (recommendedValue && !updatedCategories[catDef.key]?.model) {
updatedCategories[catDef.key] = {
...updatedCategories[catDef.key],
model: recommendedValue,
};
if (!isSlim && onCategoriesChange) {
const updatedCategories = { ...categories };
for (const catDef of OMO_BUILTIN_CATEGORIES) {
const recommendedValue = resolveRecommendedModel(catDef.recommended);
if (recommendedValue && !updatedCategories[catDef.key]?.model) {
updatedCategories[catDef.key] = {
...updatedCategories[catDef.key],
model: recommendedValue,
};
}
}
onCategoriesChange(updatedCategories);
}
onCategoriesChange(updatedCategories);
};
const configuredAgentCount = Object.keys(agents).length;
const configuredCategoryCount = Object.keys(categories).length;
const mainAgents = OMO_BUILTIN_AGENTS.filter((a) => a.group === "main");
const subAgents = OMO_BUILTIN_AGENTS.filter((a) => a.group === "sub");
const configuredCategoryCount = isSlim ? 0 : Object.keys(categories).length;
const mainAgents = builtinAgentDefs.filter((a) => a.group === "main");
const subAgents = builtinAgentDefs.filter((a) => a.group === "sub");
const readLocalFile = useReadOmoLocalFile();
const readSlimLocalFile = useReadOmoSlimLocalFile();
const [localFilePath, setLocalFilePath] = useState<string | null>(null);
const handleImportFromLocal = useCallback(async () => {
try {
const data = await readLocalFile.mutateAsync();
const data = isSlim
? await readSlimLocalFile.mutateAsync()
: await readLocalFile.mutateAsync();
const importedAgents =
(data.agents as Record<string, Record<string, unknown>> | undefined) ||
{};
@@ -753,16 +772,20 @@ export function OmoFormFields({
| undefined) || {};
onAgentsChange(importedAgents);
onCategoriesChange(importedCategories);
if (!isSlim && onCategoriesChange) {
onCategoriesChange(importedCategories);
}
onOtherFieldsStrChange(
data.otherFields ? JSON.stringify(data.otherFields, null, 2) : "",
);
setAgentAdvancedDrafts({});
setCategoryAdvancedDrafts({});
setCustomAgents(collectCustomModels(importedAgents, BUILTIN_AGENT_KEYS));
setCustomCategories(
collectCustomModels(importedCategories, BUILTIN_CATEGORY_KEYS),
);
setCustomAgents(collectCustomModels(importedAgents, builtinAgentKeys));
if (!isSlim) {
setCustomCategories(
collectCustomModels(importedCategories, BUILTIN_CATEGORY_KEYS),
);
}
setLocalFilePath(data.filePath);
toast.success(
t("omo.importLocalReplaceSuccess", {
@@ -792,7 +815,7 @@ export function OmoFormFields({
) => {
const isAgent = scope === "agent";
const store = isAgent ? agents : categories;
const setter = isAgent ? onAgentsChange : onCategoriesChange;
const setter = isAgent ? onAgentsChange : onCategoriesChange!;
const drafts = isAgent ? agentAdvancedDrafts : categoryAdvancedDrafts;
const expanded = isAgent ? expandedAgents : expandedCategories;
@@ -866,7 +889,7 @@ export function OmoFormFields({
) => {
const isAgent = scope === "agent";
const store = isAgent ? agents : categories;
const setter = isAgent ? onAgentsChange : onCategoriesChange;
const setter = isAgent ? onAgentsChange : onCategoriesChange!;
const drafts = isAgent ? agentAdvancedDrafts : categoryAdvancedDrafts;
const expanded = isAgent ? expandedAgents : expandedCategories;
const customs = isAgent ? customAgents : customCategories;
@@ -1153,30 +1176,31 @@ export function OmoFormFields({
),
})}
{renderModelSection({
title: t("omo.categories", { defaultValue: "Categories" }),
isOpen: categoriesOpen,
onToggle: () => setCategoriesOpen(!categoriesOpen),
badge: `${OMO_BUILTIN_CATEGORIES.length + customCategories.length}`,
action: renderCustomAddButton(() => addCustomModel("category")),
children: (
<>
{OMO_BUILTIN_CATEGORIES.map(renderCategoryRow)}
{customCategories.length > 0 && (
<>
{renderCustomDivider(
t("omo.customCategories", {
defaultValue: "Custom Categories",
}),
)}
{customCategories.map((c, i) =>
renderCustomModelRow("category", c, i),
)}
</>
)}
</>
),
})}
{!isSlim &&
renderModelSection({
title: t("omo.categories", { defaultValue: "Categories" }),
isOpen: categoriesOpen,
onToggle: () => setCategoriesOpen(!categoriesOpen),
badge: `${OMO_BUILTIN_CATEGORIES.length + customCategories.length}`,
action: renderCustomAddButton(() => addCustomModel("category")),
children: (
<>
{OMO_BUILTIN_CATEGORIES.map(renderCategoryRow)}
{customCategories.length > 0 && (
<>
{renderCustomDivider(
t("omo.customCategories", {
defaultValue: "Custom Categories",
}),
)}
{customCategories.map((c, i) =>
renderCustomModelRow("category", c, i),
)}
</>
)}
</>
),
})}
{renderModelSection({
title: t("omo.otherFieldsJson", {
@@ -40,11 +40,18 @@ import {
OMO_BACKGROUND_TASK_PLACEHOLDER,
OMO_BROWSER_AUTOMATION_PLACEHOLDER,
OMO_CLAUDE_CODE_PLACEHOLDER,
OMO_SLIM_DISABLEABLE_AGENTS,
OMO_SLIM_DISABLEABLE_MCPS,
OMO_SLIM_DISABLEABLE_HOOKS,
OMO_SLIM_DEFAULT_SCHEMA_URL,
} from "@/types/omo";
import {
useOmoGlobalConfig,
useSaveOmoGlobalConfig,
useReadOmoLocalFile,
useOmoSlimGlobalConfig,
useSaveOmoSlimGlobalConfig,
useReadOmoSlimLocalFile,
} from "@/lib/query/omo";
interface PresetOption {
@@ -61,6 +68,7 @@ export interface OmoGlobalConfigFieldsRef {
interface OmoGlobalConfigFieldsProps {
onStateChange?: (config: OmoGlobalConfig) => void;
hideSaveButtons?: boolean;
isSlim?: boolean;
}
type OmoAdvancedFieldKey =
@@ -114,6 +122,11 @@ const OMO_ADVANCED_JSON_FIELDS: ReadonlyArray<{
},
];
const OMO_SLIM_ADVANCED_KEYS: ReadonlySet<OmoAdvancedFieldKey> = new Set([
"lspStr",
"experimentalStr",
]);
function TagListEditor({
label,
values,
@@ -310,12 +323,25 @@ function JsonTextareaField({
export const OmoGlobalConfigFields = forwardRef<
OmoGlobalConfigFieldsRef,
OmoGlobalConfigFieldsProps
>(function OmoGlobalConfigFields({ onStateChange, hideSaveButtons }, ref) {
>(function OmoGlobalConfigFields(
{ onStateChange, hideSaveButtons, isSlim = false },
ref,
) {
const { t } = useTranslation();
const { data: config } = useOmoGlobalConfig();
const saveMutation = useSaveOmoGlobalConfig();
const { data: standardConfig } = useOmoGlobalConfig(!isSlim);
const { data: slimConfig } = useOmoSlimGlobalConfig(isSlim);
const config = isSlim ? slimConfig : standardConfig;
const standardSaveMutation = useSaveOmoGlobalConfig();
const slimSaveMutation = useSaveOmoSlimGlobalConfig();
const saveMutation = isSlim ? slimSaveMutation : standardSaveMutation;
const standardReadLocal = useReadOmoLocalFile();
const slimReadLocal = useReadOmoSlimLocalFile();
const [schemaUrl, setSchemaUrl] = useState(OMO_DEFAULT_SCHEMA_URL);
const defaultSchemaUrl = isSlim
? OMO_SLIM_DEFAULT_SCHEMA_URL
: OMO_DEFAULT_SCHEMA_URL;
const [schemaUrl, setSchemaUrl] = useState(defaultSchemaUrl);
const [sisyphusAgentStr, setSisyphusAgentStr] = useState("");
const [disabledAgents, setDisabledAgents] = useState<string[]>([]);
const [disabledMcps, setDisabledMcps] = useState<string[]>([]);
@@ -330,7 +356,7 @@ export const OmoGlobalConfigFields = forwardRef<
const [loaded, setLoaded] = useState(false);
const applyGlobalState = useCallback((global: OmoGlobalConfig) => {
setSchemaUrl(global.schemaUrl || OMO_DEFAULT_SCHEMA_URL);
setSchemaUrl(global.schemaUrl || defaultSchemaUrl);
setSisyphusAgentStr(
global.sisyphusAgent ? JSON.stringify(global.sisyphusAgent, null, 2) : "",
);
@@ -545,7 +571,7 @@ export const OmoGlobalConfigFields = forwardRef<
placeholder: t("omo.disabledAgentsPlaceholder", {
defaultValue: "Disabled Agents",
}),
presets: OMO_DISABLEABLE_AGENTS,
presets: isSlim ? OMO_SLIM_DISABLEABLE_AGENTS : OMO_DISABLEABLE_AGENTS,
},
{
key: "mcps",
@@ -555,7 +581,7 @@ export const OmoGlobalConfigFields = forwardRef<
placeholder: t("omo.disabledMcpsPlaceholder", {
defaultValue: "Disabled MCPs",
}),
presets: OMO_DISABLEABLE_MCPS,
presets: isSlim ? OMO_SLIM_DISABLEABLE_MCPS : OMO_DISABLEABLE_MCPS,
},
{
key: "hooks",
@@ -565,21 +591,25 @@ export const OmoGlobalConfigFields = forwardRef<
placeholder: t("omo.disabledHooksPlaceholder", {
defaultValue: "Disabled Hooks",
}),
presets: OMO_DISABLEABLE_HOOKS,
presets: isSlim ? OMO_SLIM_DISABLEABLE_HOOKS : OMO_DISABLEABLE_HOOKS,
},
{
key: "skills",
label: t("omo.disabledSkills", { defaultValue: "Skills" }),
values: disabledSkills,
onChange: setDisabledSkills,
placeholder: t("omo.disabledSkillsPlaceholder", {
defaultValue: "Disabled Skills",
}),
presets: OMO_DISABLEABLE_SKILLS,
},
] as const;
...(!isSlim
? [
{
key: "skills" as const,
label: t("omo.disabledSkills", { defaultValue: "Skills" }),
values: disabledSkills,
onChange: setDisabledSkills,
placeholder: t("omo.disabledSkillsPlaceholder", {
defaultValue: "Disabled Skills",
}),
presets: OMO_DISABLEABLE_SKILLS,
},
]
: []),
];
const readLocalFile = useReadOmoLocalFile();
const readLocalFile = isSlim ? slimReadLocal : standardReadLocal;
const handleImportGlobalFromLocal = useCallback(async () => {
try {
@@ -668,25 +698,27 @@ export const OmoGlobalConfigFields = forwardRef<
<Input
value={schemaUrl}
onChange={(e) => setSchemaUrl(e.target.value)}
placeholder={OMO_DEFAULT_SCHEMA_URL}
placeholder={defaultSchemaUrl}
className="text-sm h-8"
/>
</div>
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-2">
<Label className="text-sm font-semibold">
{t("omo.sisyphusAgentConfig", {
defaultValue: "Sisyphus Agent",
})}
</Label>
<Textarea
value={sisyphusAgentStr}
onChange={(e) => setSisyphusAgentStr(e.target.value)}
placeholder={OMO_SISYPHUS_AGENT_PLACEHOLDER}
className="font-mono text-sm"
style={{ minHeight: "140px" }}
/>
</div>
{!isSlim && (
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-2">
<Label className="text-sm font-semibold">
{t("omo.sisyphusAgentConfig", {
defaultValue: "Sisyphus Agent",
})}
</Label>
<Textarea
value={sisyphusAgentStr}
onChange={(e) => setSisyphusAgentStr(e.target.value)}
placeholder={OMO_SISYPHUS_AGENT_PLACEHOLDER}
className="font-mono text-sm"
style={{ minHeight: "140px" }}
/>
</div>
)}
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-3">
<div className="flex items-center gap-2">
@@ -715,7 +747,9 @@ export const OmoGlobalConfigFields = forwardRef<
<Label className="text-sm font-semibold">
{t("omo.advanced", { defaultValue: "Advanced Settings" })}
</Label>
{OMO_ADVANCED_JSON_FIELDS.map((field) => (
{OMO_ADVANCED_JSON_FIELDS.filter(
(field) => !isSlim || OMO_SLIM_ADVANCED_KEYS.has(field.key),
).map((field) => (
<JsonTextareaField
key={field.key}
label={t(field.labelKey, { defaultValue: field.defaultLabel })}
+46 -21
View File
@@ -78,7 +78,7 @@ import {
useOmoDraftState,
useOpenclawFormState,
} from "./hooks";
import { useOmoGlobalConfig } from "@/lib/query/omo";
import { useOmoGlobalConfig, useOmoSlimGlobalConfig } from "@/lib/query/omo";
import {
CLAUDE_DEFAULT_CONFIG,
CODEX_DEFAULT_CONFIG,
@@ -184,7 +184,15 @@ export function ProviderForm({
initialCategory: initialData?.category,
});
const isOmoCategory = appId === "opencode" && category === "omo";
const { data: queriedOmoGlobalConfig } = useOmoGlobalConfig(isOmoCategory);
const isOmoSlimCategory = appId === "opencode" && category === "omo-slim";
const isAnyOmoCategory = isOmoCategory || isOmoSlimCategory;
const { data: queriedStandardOmoGlobalConfig } =
useOmoGlobalConfig(isOmoCategory);
const { data: queriedSlimOmoGlobalConfig } =
useOmoSlimGlobalConfig(isOmoSlimCategory);
const queriedOmoGlobalConfig = isOmoSlimCategory
? queriedSlimOmoGlobalConfig
: queriedStandardOmoGlobalConfig;
useEffect(() => {
setSelectedPresetId(initialData ? null : "custom");
@@ -495,7 +503,7 @@ export function ProviderForm({
omoModelVariantsMap,
omoPresetMetaMap,
existingOpencodeKeys,
} = useOmoModelSource({ isOmoCategory, providerId });
} = useOmoModelSource({ isOmoCategory: isAnyOmoCategory, providerId });
const opencodeForm = useOpencodeFormState({
initialData,
@@ -506,7 +514,8 @@ export function ProviderForm({
});
const initialOmoSettings =
appId === "opencode" && initialData?.category === "omo"
appId === "opencode" &&
(initialData?.category === "omo" || initialData?.category === "omo-slim")
? (initialData.settingsConfig as Record<string, unknown> | undefined)
: undefined;
@@ -677,13 +686,19 @@ export function ProviderForm({
} catch (err) {
settingsConfig = values.settingsConfig.trim();
}
} else if (appId === "opencode" && category === "omo") {
} else if (
appId === "opencode" &&
(category === "omo" || category === "omo-slim")
) {
const omoConfig: Record<string, unknown> = {};
omoConfig.useCommonConfig = omoDraft.useOmoCommonConfig;
if (Object.keys(omoDraft.omoAgents).length > 0) {
omoConfig.agents = omoDraft.omoAgents;
}
if (Object.keys(omoDraft.omoCategories).length > 0) {
if (
category === "omo" &&
Object.keys(omoDraft.omoCategories).length > 0
) {
omoConfig.categories = omoDraft.omoCategories;
}
if (omoDraft.omoOtherFieldsStr.trim()) {
@@ -1334,19 +1349,25 @@ export function ProviderForm({
/>
)}
{appId === "opencode" && category === "omo" && (
<OmoFormFields
modelOptions={omoModelOptions}
modelVariantsMap={omoModelVariantsMap}
presetMetaMap={omoPresetMetaMap}
agents={omoDraft.omoAgents}
onAgentsChange={omoDraft.setOmoAgents}
categories={omoDraft.omoCategories}
onCategoriesChange={omoDraft.setOmoCategories}
otherFieldsStr={omoDraft.omoOtherFieldsStr}
onOtherFieldsStrChange={omoDraft.setOmoOtherFieldsStr}
/>
)}
{appId === "opencode" &&
(category === "omo" || category === "omo-slim") && (
<OmoFormFields
modelOptions={omoModelOptions}
modelVariantsMap={omoModelVariantsMap}
presetMetaMap={omoPresetMetaMap}
agents={omoDraft.omoAgents}
onAgentsChange={omoDraft.setOmoAgents}
categories={
category === "omo" ? omoDraft.omoCategories : undefined
}
onCategoriesChange={
category === "omo" ? omoDraft.setOmoCategories : undefined
}
otherFieldsStr={omoDraft.omoOtherFieldsStr}
onOtherFieldsStrChange={omoDraft.setOmoOtherFieldsStr}
isSlim={category === "omo-slim"}
/>
)}
{/* OpenClaw 专属字段 */}
{appId === "openclaw" && (
@@ -1408,7 +1429,8 @@ export function ProviderForm({
/>
{settingsConfigErrorField}
</>
) : appId === "opencode" && category === "omo" ? (
) : appId === "opencode" &&
(category === "omo" || category === "omo-slim") ? (
<OmoCommonConfigEditor
previewValue={omoDraft.mergedOmoJsonPreview}
useCommonConfig={omoDraft.useOmoCommonConfig}
@@ -1421,8 +1443,11 @@ export function ProviderForm({
onGlobalConfigStateChange={omoDraft.setOmoGlobalState}
globalConfigRef={omoDraft.omoGlobalConfigRef}
fieldsKey={omoDraft.omoFieldsKey}
isSlim={category === "omo-slim"}
/>
) : appId === "opencode" && category !== "omo" ? (
) : appId === "opencode" &&
category !== "omo" &&
category !== "omo-slim" ? (
<>
<div className="space-y-2">
<Label htmlFor="settingsConfig">{t("provider.configJson")}</Label>
@@ -2,7 +2,11 @@ import { useState, useCallback, useEffect, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import type { OmoGlobalConfig } from "@/types/omo";
import { mergeOmoConfigPreview } from "@/types/omo";
import {
mergeOmoConfigPreview,
mergeOmoSlimConfigPreview,
buildOmoSlimProfilePreview,
} from "@/types/omo";
import { type OmoGlobalConfigFieldsRef } from "../OmoGlobalConfigFields";
import * as configApi from "@/lib/api/config";
import {
@@ -54,6 +58,8 @@ export function useOmoDraftState({
category,
}: UseOmoDraftStateParams): OmoDraftState {
const { t } = useTranslation();
const isSlim = category === "omo-slim";
const commonConfigKey = isSlim ? "omo_slim" : "omo";
const [omoAgents, setOmoAgents] = useState<
Record<string, Record<string, unknown>>
@@ -93,6 +99,14 @@ export function useOmoDraftState({
const mergedOmoJsonPreview = useMemo(() => {
if (useOmoCommonConfig) {
if (isSlim) {
const merged = mergeOmoSlimConfigPreview(
effectiveOmoGlobalConfig,
omoAgents,
omoOtherFieldsStr,
);
return JSON.stringify(merged, null, 2);
}
const merged = mergeOmoConfigPreview(
effectiveOmoGlobalConfig,
omoAgents,
@@ -101,6 +115,13 @@ export function useOmoDraftState({
);
return JSON.stringify(merged, null, 2);
} else {
if (isSlim) {
return JSON.stringify(
buildOmoSlimProfilePreview(omoAgents, omoOtherFieldsStr),
null,
2,
);
}
return JSON.stringify(
buildOmoProfilePreview(omoAgents, omoCategories, omoOtherFieldsStr),
null,
@@ -113,16 +134,22 @@ export function useOmoDraftState({
omoAgents,
omoCategories,
omoOtherFieldsStr,
isSlim,
]);
// Auto-detect whether common config has content for new OMO profiles
// Auto-detect whether common config has content for new OMO/OMO Slim profiles
useEffect(() => {
if (appId !== "opencode" || category !== "omo" || isEditMode) return;
if (
appId !== "opencode" ||
(category !== "omo" && category !== "omo-slim") ||
isEditMode
)
return;
let active = true;
(async () => {
let next = false;
try {
const raw = await configApi.getCommonConfigSnippet("omo");
const raw = await configApi.getCommonConfigSnippet(commonConfigKey);
if (raw) {
const parsed = JSON.parse(raw) as Record<string, unknown>;
next = Object.keys(parsed).some(
@@ -135,14 +162,17 @@ export function useOmoDraftState({
return () => {
active = false;
};
}, [appId, category, isEditMode]);
}, [appId, category, isEditMode, commonConfigKey]);
const handleOmoGlobalConfigSave = useCallback(async () => {
if (!omoGlobalConfigRef.current) return;
setIsOmoSaving(true);
try {
const config = omoGlobalConfigRef.current.buildCurrentConfigStrict();
await configApi.setCommonConfigSnippet("omo", JSON.stringify(config));
await configApi.setCommonConfigSnippet(
commonConfigKey,
JSON.stringify(config),
);
setIsOmoConfigModalOpen(false);
toast.success(
t("omo.globalConfigSaved", { defaultValue: "Global config saved" }),
@@ -152,7 +182,7 @@ export function useOmoDraftState({
} finally {
setIsOmoSaving(false);
}
}, [t]);
}, [t, commonConfigKey]);
const handleOmoEditClick = useCallback(() => {
setOmoFieldsKey((k) => k + 1);
@@ -133,7 +133,7 @@ export function useOmoModelSource({
const parseFailedProviders: string[] = [];
for (const [providerKey, provider] of Object.entries(allProviders)) {
if (provider.category === "omo") {
if (provider.category === "omo" || provider.category === "omo-slim") {
continue;
}
if (liveSet && !liveSet.has(providerKey)) {
+13
View File
@@ -1129,4 +1129,17 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
iconColor: "#8B5CF6",
isCustomTemplate: true,
},
{
name: "Oh My OpenCode Slim",
websiteUrl: "https://github.com/alvinunreal/oh-my-opencode-slim",
settingsConfig: {
npm: "",
options: {},
models: {},
},
category: "omo-slim" as ProviderCategory,
icon: "opencode",
iconColor: "#6366F1",
isCustomTemplate: true,
},
];
+16
View File
@@ -1858,6 +1858,22 @@
"unspecifiedLow": "Uncategorized low-effort task category for tasks that don't fit other categories with small workload. Defaults to Claude Sonnet 4.5.",
"unspecifiedHigh": "Uncategorized high-effort task category for tasks that don't fit other categories with large workload. Defaults to Claude Opus 4.6 max variant.",
"writing": "Writing category for documentation, prose and technical writing. Defaults to Gemini 3 Flash fast generation model."
},
"slimAgentDesc": {
"orchestrator": "Orchestrator",
"oracle": "Oracle",
"librarian": "Librarian",
"explorer": "Explorer",
"designer": "Designer",
"fixer": "Fixer"
},
"slimAgentTooltip": {
"orchestrator": "Writes executable code, orchestrates multi-agent workflow, summons experts",
"oracle": "Root cause analysis, architecture review, debugging guidance (read-only)",
"librarian": "Documentation lookup, GitHub code search (read-only)",
"explorer": "Regex search, AST pattern matching, file discovery (read-only)",
"designer": "Modern responsive design, CSS/Tailwind expertise",
"fixer": "Code implementation, refactoring, testing, verification"
}
},
"openclawConfig": {
+16
View File
@@ -1839,6 +1839,22 @@
"unspecifiedLow": "未分類の低作業量タスクカテゴリ。他のカテゴリに該当せず作業量が小さいタスクに適用。デフォルトで Claude Sonnet 4.5 を使用。",
"unspecifiedHigh": "未分類の高作業量タスクカテゴリ。他のカテゴリに該当せず作業量が大きいタスクに適用。デフォルトで Claude Opus 4.6 の max バリアントを使用。",
"writing": "ライティングカテゴリ。ドキュメント、散文、技術文書に特化。デフォルトで Gemini 3 Flash 高速生成モデルを使用。"
},
"slimAgentDesc": {
"orchestrator": "オーケストレーター",
"oracle": "オラクル",
"librarian": "ライブラリアン",
"explorer": "エクスプローラー",
"designer": "デザイナー",
"fixer": "フィクサー"
},
"slimAgentTooltip": {
"orchestrator": "実行コードの作成、マルチエージェントワークフローの調整、エキスパートの召喚",
"oracle": "根本原因分析、アーキテクチャレビュー、デバッグガイダンス(読み取り専用)",
"librarian": "ドキュメント検索、GitHubコード検索(読み取り専用)",
"explorer": "正規表現検索、ASTパターンマッチング、ファイル検出(読み取り専用)",
"designer": "モダンなレスポンシブデザイン、CSS/Tailwindの専門知識",
"fixer": "コード実装、リファクタリング、テスト、検証"
}
},
"openclawConfig": {
+16
View File
@@ -1858,6 +1858,22 @@
"unspecifiedLow": "未归类低工作量任务类别,适用于不适合其他类别且工作量较小的任务,默认使用 Claude Sonnet 4.5。",
"unspecifiedHigh": "未归类高工作量任务类别,适用于不适合其他类别且工作量较大的任务,默认使用 Claude Opus 4.6 的最大变体。",
"writing": "写作类别,专注于文档、散文和技术写作,默认使用 Gemini 3 Flash 快速生成模型。"
},
"slimAgentDesc": {
"orchestrator": "编排者",
"oracle": "神谕者",
"librarian": "图书管理员",
"explorer": "探索者",
"designer": "设计师",
"fixer": "修复者"
},
"slimAgentTooltip": {
"orchestrator": "编写执行代码,编排多代理工作流,召唤专家",
"oracle": "根本原因分析、架构审查、调试指导(只读)",
"librarian": "文档查询、GitHub 代码搜索(只读)",
"explorer": "正则搜索、AST 模式匹配、文件发现(只读)",
"designer": "现代响应式设计、CSS/Tailwind 精通",
"fixer": "代码实现、重构、测试、验证"
}
},
"openclawConfig": {
+1 -1
View File
@@ -1,7 +1,7 @@
// 配置相关 API
import { invoke } from "@tauri-apps/api/core";
export type AppType = "claude" | "codex" | "gemini" | "omo";
export type AppType = "claude" | "codex" | "gemini" | "omo" | "omo_slim";
/**
* 获取 Claude 通用配置片段(已废弃,使用 getCommonConfigSnippet
+10
View File
@@ -8,3 +8,13 @@ export const omoApi = {
getOmoProviderCount: (): Promise<number> => invoke("get_omo_provider_count"),
disableCurrentOmo: (): Promise<void> => invoke("disable_current_omo"),
};
export const omoSlimApi = {
readLocalFile: (): Promise<OmoLocalFileData> =>
invoke("read_omo_slim_local_file"),
getCurrentProviderId: (): Promise<string> =>
invoke("get_current_omo_slim_provider_id"),
getProviderCount: (): Promise<number> =>
invoke("get_omo_slim_provider_count"),
disableCurrent: (): Promise<void> => invoke("disable_current_omo_slim"),
};
+100 -1
View File
@@ -1,5 +1,5 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { omoApi } from "@/lib/api/omo";
import { omoApi, omoSlimApi } from "@/lib/api/omo";
import * as configApi from "@/lib/api/config";
import type { OmoGlobalConfig } from "@/types/omo";
@@ -93,3 +93,102 @@ export function useDisableCurrentOmo() {
onSuccess: () => invalidateOmoQueries(queryClient),
});
}
// ============================================================================
// OMO Slim query hooks
// ============================================================================
export const omoSlimKeys = {
all: ["omo-slim"] as const,
globalConfig: () => [...omoSlimKeys.all, "global-config"] as const,
currentProviderId: () => [...omoSlimKeys.all, "current-provider-id"] as const,
providerCount: () => [...omoSlimKeys.all, "provider-count"] as const,
};
function invalidateOmoSlimQueries(
queryClient: ReturnType<typeof useQueryClient>,
) {
queryClient.invalidateQueries({ queryKey: omoSlimKeys.globalConfig() });
queryClient.invalidateQueries({ queryKey: ["providers"] });
queryClient.invalidateQueries({
queryKey: omoSlimKeys.currentProviderId(),
});
queryClient.invalidateQueries({ queryKey: omoSlimKeys.providerCount() });
}
export function useOmoSlimGlobalConfig(enabled = true) {
return useQuery({
queryKey: omoSlimKeys.globalConfig(),
enabled,
queryFn: async (): Promise<OmoGlobalConfig> => {
const raw = await configApi.getCommonConfigSnippet("omo_slim");
if (!raw) {
return {
id: "global",
disabledAgents: [],
disabledMcps: [],
disabledHooks: [],
disabledSkills: [],
updatedAt: new Date().toISOString(),
};
}
try {
return JSON.parse(raw) as OmoGlobalConfig;
} catch (error) {
console.warn(
"[omo-slim] invalid global config json, fallback to defaults",
error,
);
return {
id: "global",
disabledAgents: [],
disabledMcps: [],
disabledHooks: [],
disabledSkills: [],
updatedAt: new Date().toISOString(),
};
}
},
});
}
export function useCurrentOmoSlimProviderId(enabled = true) {
return useQuery({
queryKey: omoSlimKeys.currentProviderId(),
queryFn: omoSlimApi.getCurrentProviderId,
enabled,
});
}
export function useOmoSlimProviderCount(enabled = true) {
return useQuery({
queryKey: omoSlimKeys.providerCount(),
queryFn: omoSlimApi.getProviderCount,
enabled,
});
}
export function useSaveOmoSlimGlobalConfig() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: OmoGlobalConfig) => {
const jsonStr = JSON.stringify(input);
await configApi.setCommonConfigSnippet("omo_slim", jsonStr);
},
onSuccess: () => invalidateOmoSlimQueries(queryClient),
});
}
export function useReadOmoSlimLocalFile() {
return useMutation({
mutationFn: () => omoSlimApi.readLocalFile(),
});
}
export function useDisableCurrentOmoSlim() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => omoSlimApi.disableCurrent(),
onSuccess: () => invalidateOmoSlimQueries(queryClient),
});
}
+2 -1
View File
@@ -4,7 +4,8 @@ export type ProviderCategory =
| "aggregator" // 聚合网站
| "third_party" // 第三方供应商
| "custom" // 自定义
| "omo"; // Oh My OpenCode
| "omo" // Oh My OpenCode
| "omo-slim"; // Oh My OpenCode Slim
export interface Provider {
id: string;
+136
View File
@@ -327,6 +327,142 @@ export function parseOmoOtherFieldsObject(
return parsed as Record<string, unknown>;
}
// ============================================================================
// OMO Slim (oh-my-opencode-slim) definitions
// ============================================================================
export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
{
key: "orchestrator",
display: "Orchestrator",
descKey: "omo.slimAgentDesc.orchestrator",
tooltipKey: "omo.slimAgentTooltip.orchestrator",
recommended: "kimi-for-coding/k2p5",
group: "main",
},
{
key: "oracle",
display: "Oracle",
descKey: "omo.slimAgentDesc.oracle",
tooltipKey: "omo.slimAgentTooltip.oracle",
recommended: "openai/gpt-5.2-codex",
group: "sub",
},
{
key: "librarian",
display: "Librarian",
descKey: "omo.slimAgentDesc.librarian",
tooltipKey: "omo.slimAgentTooltip.librarian",
recommended: "openai/gpt-5.1-codex-mini",
group: "sub",
},
{
key: "explorer",
display: "Explorer",
descKey: "omo.slimAgentDesc.explorer",
tooltipKey: "omo.slimAgentTooltip.explorer",
recommended: "openai/gpt-5.1-codex-mini",
group: "sub",
},
{
key: "designer",
display: "Designer",
descKey: "omo.slimAgentDesc.designer",
tooltipKey: "omo.slimAgentTooltip.designer",
recommended: "kimi-for-coding/k2p5",
group: "sub",
},
{
key: "fixer",
display: "Fixer",
descKey: "omo.slimAgentDesc.fixer",
tooltipKey: "omo.slimAgentTooltip.fixer",
recommended: "openai/gpt-5.1-codex-mini",
group: "sub",
},
];
export const OMO_SLIM_DISABLEABLE_AGENTS = [
{ value: "orchestrator", label: "Orchestrator" },
{ value: "oracle", label: "Oracle" },
{ value: "librarian", label: "Librarian" },
{ value: "explorer", label: "Explorer" },
{ value: "designer", label: "Designer" },
{ value: "fixer", label: "Fixer" },
] as const;
export const OMO_SLIM_DISABLEABLE_MCPS = [
{ value: "context7", label: "context7" },
{ value: "grep_app", label: "grep_app" },
{ value: "websearch", label: "websearch" },
] as const;
export const OMO_SLIM_DISABLEABLE_HOOKS = [
{ value: "auto-update-checker", label: "auto-update-checker" },
{ value: "phase-reminder", label: "phase-reminder" },
{ value: "post-read-nudge", label: "post-read-nudge" },
] as const;
export const OMO_SLIM_DEFAULT_SCHEMA_URL =
"https://raw.githubusercontent.com/alvinunreal/oh-my-opencode-slim/master/assets/oh-my-opencode-slim.schema.json";
export function mergeOmoSlimConfigPreview(
global: OmoGlobalConfig | undefined,
agents: Record<string, Record<string, unknown>>,
otherFieldsStr: string,
): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (global) {
if (global.schemaUrl) result["$schema"] = global.schemaUrl;
if (global.disabledAgents?.length)
result["disabled_agents"] = global.disabledAgents;
if (global.disabledMcps?.length)
result["disabled_mcps"] = global.disabledMcps;
if (global.disabledHooks?.length)
result["disabled_hooks"] = global.disabledHooks;
if (global.otherFields) {
for (const [k, v] of Object.entries(global.otherFields)) {
result[k] = v;
}
}
}
if (Object.keys(agents).length > 0) result["agents"] = agents;
try {
const other = parseOmoOtherFieldsObject(otherFieldsStr);
if (other) {
for (const [k, v] of Object.entries(other)) {
result[k] = v;
}
}
} catch {}
return result;
}
export function buildOmoSlimProfilePreview(
agents: Record<string, Record<string, unknown>>,
otherFieldsStr: string,
): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (Object.keys(agents).length > 0) result["agents"] = agents;
try {
const other = parseOmoOtherFieldsObject(otherFieldsStr);
if (other) {
for (const [k, v] of Object.entries(other)) {
result[k] = v;
}
}
} catch {}
return result;
}
export function mergeOmoConfigPreview(
global: OmoGlobalConfig,
agents: Record<string, Record<string, unknown>>,