mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 23:56:02 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1905765821 | |||
| 651496eabb | |||
| a301a81f7a | |||
| 3f50afbbf2 | |||
| b551bb92e2 | |||
| 3e9b3d0031 | |||
| d9f380d0fd | |||
| 300abc31a7 |
@@ -57,6 +57,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
|||||||
Generated
+39
@@ -59,6 +59,9 @@ importers:
|
|||||||
'@radix-ui/react-label':
|
'@radix-ui/react-label':
|
||||||
specifier: ^2.1.7
|
specifier: ^2.1.7
|
||||||
version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-popover':
|
||||||
|
specifier: ^1.1.15
|
||||||
|
version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-scroll-area':
|
'@radix-ui/react-scroll-area':
|
||||||
specifier: ^1.2.10
|
specifier: ^1.2.10
|
||||||
version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -1048,6 +1051,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-popover@1.1.15':
|
||||||
|
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-popper@1.2.8':
|
'@radix-ui/react-popper@1.2.8':
|
||||||
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
|
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3890,6 +3906,29 @@ snapshots:
|
|||||||
'@types/react': 18.3.23
|
'@types/react': 18.3.23
|
||||||
'@types/react-dom': 18.3.7(@types/react@18.3.23)
|
'@types/react-dom': 18.3.7(@types/react@18.3.23)
|
||||||
|
|
||||||
|
'@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.23)(react@18.3.1)
|
||||||
|
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
|
||||||
|
'@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
|
||||||
|
aria-hidden: 1.2.6
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.23
|
||||||
|
'@types/react-dom': 18.3.7(@types/react@18.3.23)
|
||||||
|
|
||||||
'@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
|||||||
let mut search_paths: Vec<std::path::PathBuf> = vec![
|
let mut search_paths: Vec<std::path::PathBuf> = vec![
|
||||||
home.join(".local/bin"), // Native install (official recommended)
|
home.join(".local/bin"), // Native install (official recommended)
|
||||||
home.join(".npm-global/bin"),
|
home.join(".npm-global/bin"),
|
||||||
home.join("n/bin"), // n version manager
|
home.join("n/bin"), // n version manager
|
||||||
home.join(".volta/bin"), // Volta package manager
|
home.join(".volta/bin"), // Volta package manager
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -286,10 +286,22 @@ impl OmoService {
|
|||||||
|
|
||||||
let obj = Self::read_jsonc_object(&actual_path)?;
|
let obj = Self::read_jsonc_object(&actual_path)?;
|
||||||
|
|
||||||
|
Ok(Self::build_local_file_data_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,
|
||||||
|
last_modified: Option<String>,
|
||||||
|
) -> OmoLocalFileData {
|
||||||
let agents = obj.get("agents").cloned();
|
let agents = obj.get("agents").cloned();
|
||||||
let categories = obj.get("categories").cloned();
|
let categories = obj.get("categories").cloned();
|
||||||
|
|
||||||
let other = Self::extract_other_fields(&obj);
|
let other = Self::extract_other_fields(obj);
|
||||||
let other_fields = if other.is_empty() {
|
let other_fields = if other.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -297,16 +309,17 @@ impl OmoService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut global = OmoGlobalConfig::default();
|
let mut global = OmoGlobalConfig::default();
|
||||||
Self::merge_global_from_obj(&obj, &mut global);
|
Self::merge_global_from_obj(obj, &mut global);
|
||||||
|
global.other_fields = other_fields.clone();
|
||||||
|
|
||||||
Ok(OmoLocalFileData {
|
OmoLocalFileData {
|
||||||
agents,
|
agents,
|
||||||
categories,
|
categories,
|
||||||
other_fields,
|
other_fields,
|
||||||
global,
|
global,
|
||||||
file_path: actual_path.to_string_lossy().to_string(),
|
file_path,
|
||||||
last_modified,
|
last_modified,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strip_jsonc_comments(input: &str) -> String {
|
fn strip_jsonc_comments(input: &str) -> String {
|
||||||
@@ -400,7 +413,7 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let agents = Some(serde_json::json!({
|
let agents = Some(serde_json::json!({
|
||||||
"Sisyphus": { "model": "claude-opus-4-5" }
|
"sisyphus": { "model": "claude-opus-4-5" }
|
||||||
}));
|
}));
|
||||||
let categories = None;
|
let categories = None;
|
||||||
let other_fields = None;
|
let other_fields = None;
|
||||||
@@ -411,7 +424,7 @@ mod tests {
|
|||||||
assert_eq!(obj["$schema"], "https://example.com/schema.json");
|
assert_eq!(obj["$schema"], "https://example.com/schema.json");
|
||||||
assert_eq!(obj["disabled_agents"], serde_json::json!(["explore"]));
|
assert_eq!(obj["disabled_agents"], serde_json::json!(["explore"]));
|
||||||
assert!(obj.contains_key("agents"));
|
assert!(obj.contains_key("agents"));
|
||||||
assert_eq!(obj["agents"]["Sisyphus"]["model"], "claude-opus-4-5");
|
assert_eq!(obj["agents"]["sisyphus"]["model"], "claude-opus-4-5");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -422,7 +435,7 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let agents = Some(serde_json::json!({
|
let agents = Some(serde_json::json!({
|
||||||
"Sisyphus": { "model": "claude-opus-4-5" }
|
"sisyphus": { "model": "claude-opus-4-5" }
|
||||||
}));
|
}));
|
||||||
let categories = None;
|
let categories = None;
|
||||||
let other_fields = None;
|
let other_fields = None;
|
||||||
@@ -434,4 +447,61 @@ mod tests {
|
|||||||
assert!(!obj.contains_key("disabled_agents"));
|
assert!(!obj.contains_key("disabled_agents"));
|
||||||
assert!(obj.contains_key("agents"));
|
assert!(obj.contains_key("agents"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_local_file_data_keeps_unknown_top_level_fields_in_global() {
|
||||||
|
let obj = serde_json::json!({
|
||||||
|
"$schema": "https://example.com/schema.json",
|
||||||
|
"disabled_agents": ["oracle"],
|
||||||
|
"agents": {
|
||||||
|
"sisyphus": { "model": "claude-opus-4-6" }
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"code": { "model": "gpt-5.3" }
|
||||||
|
},
|
||||||
|
"custom_top_level": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let obj_map = obj.as_object().unwrap().clone();
|
||||||
|
|
||||||
|
let data = OmoService::build_local_file_data_from_obj(
|
||||||
|
&obj_map,
|
||||||
|
"/tmp/oh-my-opencode.jsonc".to_string(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
data.global.schema_url.as_deref(),
|
||||||
|
Some("https://example.com/schema.json")
|
||||||
|
);
|
||||||
|
assert_eq!(data.global.disabled_agents, vec!["oracle".to_string()]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
data.other_fields,
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"custom_top_level": { "enabled": true }
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
assert_eq!(data.global.other_fields, data.other_fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_config_ignores_non_object_other_fields() {
|
||||||
|
let global = OmoGlobalConfig {
|
||||||
|
other_fields: Some(serde_json::json!(["global_non_object"])),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let agents = None;
|
||||||
|
let categories = None;
|
||||||
|
let other_fields = Some(serde_json::json!("profile_non_object"));
|
||||||
|
let profile_data = (agents, categories, other_fields, true);
|
||||||
|
|
||||||
|
let merged = OmoService::merge_config(&global, Some(&profile_data));
|
||||||
|
let obj = merged.as_object().unwrap();
|
||||||
|
|
||||||
|
assert!(!obj.contains_key("0"));
|
||||||
|
assert!(!obj.contains_key("global_non_object"));
|
||||||
|
assert!(!obj.contains_key("profile_non_object"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -141,7 +141,11 @@ function App() {
|
|||||||
|
|
||||||
// Fallback from sessions view when switching to an app without session support
|
// Fallback from sessions view when switching to an app without session support
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentView === "sessions" && activeApp !== "claude" && activeApp !== "codex") {
|
if (
|
||||||
|
currentView === "sessions" &&
|
||||||
|
activeApp !== "claude" &&
|
||||||
|
activeApp !== "codex"
|
||||||
|
) {
|
||||||
setCurrentView("providers");
|
setCurrentView("providers");
|
||||||
}
|
}
|
||||||
}, [activeApp, currentView]);
|
}, [activeApp, currentView]);
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -21,6 +34,10 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
FolderInput,
|
FolderInput,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
HelpCircle,
|
||||||
|
Check,
|
||||||
|
ChevronsUpDown,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -43,6 +60,13 @@ const ADVANCED_PLACEHOLDER = `{
|
|||||||
interface OmoFormFieldsProps {
|
interface OmoFormFieldsProps {
|
||||||
modelOptions: Array<{ value: string; label: string }>;
|
modelOptions: Array<{ value: string; label: string }>;
|
||||||
modelVariantsMap?: Record<string, string[]>;
|
modelVariantsMap?: Record<string, string[]>;
|
||||||
|
presetMetaMap?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
options?: Record<string, unknown>;
|
||||||
|
limit?: { context?: number; output?: number };
|
||||||
|
}
|
||||||
|
>;
|
||||||
agents: Record<string, Record<string, unknown>>;
|
agents: Record<string, Record<string, unknown>>;
|
||||||
onAgentsChange: (agents: Record<string, Record<string, unknown>>) => void;
|
onAgentsChange: (agents: Record<string, Record<string, unknown>>) => void;
|
||||||
categories: Record<string, Record<string, unknown>>;
|
categories: Record<string, Record<string, unknown>>;
|
||||||
@@ -53,19 +77,149 @@ interface OmoFormFieldsProps {
|
|||||||
onOtherFieldsStrChange: (value: string) => void;
|
onOtherFieldsStrChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomModelItem = { key: string; model: string };
|
export type CustomModelItem = {
|
||||||
|
key: string;
|
||||||
|
model: string;
|
||||||
|
sourceKey?: string;
|
||||||
|
};
|
||||||
type BuiltinModelDef = Pick<
|
type BuiltinModelDef = Pick<
|
||||||
OmoAgentDef | OmoCategoryDef,
|
OmoAgentDef | OmoCategoryDef,
|
||||||
"key" | "display" | "descZh" | "descEn" | "recommended"
|
"key" | "display" | "descKey" | "recommended" | "tooltipKey"
|
||||||
>;
|
>;
|
||||||
type ModelOption = { value: string; label: string };
|
type ModelOption = { value: string; label: string };
|
||||||
|
|
||||||
|
function DeferredKeyInput({
|
||||||
|
value,
|
||||||
|
onCommit,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onCommit: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const [draft, setDraft] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDraft(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
value={draft}
|
||||||
|
onChange={(e) => setDraft(e.target.value)}
|
||||||
|
onBlur={() => {
|
||||||
|
if (draft !== value) {
|
||||||
|
onCommit(draft);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const BUILTIN_AGENT_KEYS = new Set(OMO_BUILTIN_AGENTS.map((a) => a.key));
|
const BUILTIN_AGENT_KEYS = new Set(OMO_BUILTIN_AGENTS.map((a) => a.key));
|
||||||
const BUILTIN_CATEGORY_KEYS = new Set(OMO_BUILTIN_CATEGORIES.map((c) => c.key));
|
const BUILTIN_CATEGORY_KEYS = new Set(OMO_BUILTIN_CATEGORIES.map((c) => c.key));
|
||||||
const EMPTY_MODEL_VALUE = "__cc_switch_omo_model_empty__";
|
|
||||||
const UNAVAILABLE_MODEL_VALUE = "__cc_switch_omo_model_unavailable__";
|
|
||||||
const EMPTY_VARIANT_VALUE = "__cc_switch_omo_variant_empty__";
|
const EMPTY_VARIANT_VALUE = "__cc_switch_omo_variant_empty__";
|
||||||
const UNAVAILABLE_VARIANT_VALUE = "__cc_switch_omo_variant_unavailable__";
|
|
||||||
|
function ModelCombobox({
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
recommended,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
options: ModelOption[];
|
||||||
|
recommended?: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const selectedLabel = options.find((o) => o.value === value)?.label;
|
||||||
|
|
||||||
|
const selectModelText = t("omo.selectModel", {
|
||||||
|
defaultValue: "Select configured model",
|
||||||
|
});
|
||||||
|
const placeholderText = recommended
|
||||||
|
? `${selectModelText} (${t("omo.recommendedHint", { model: recommended, defaultValue: "Recommended: {{model}}" })})`
|
||||||
|
: selectModelText;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover modal open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className="flex flex-1 h-8 items-center justify-between whitespace-nowrap rounded-md border border-border-default bg-background px-3 py-1 text-sm shadow-sm ring-offset-background focus:outline-none focus-visible:outline-none focus:border-border-default focus-visible:border-border-default focus:ring-0 focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<span className={cn("truncate", !value && "text-muted-foreground")}>
|
||||||
|
{selectedLabel || placeholderText}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center shrink-0 ml-1 gap-0.5">
|
||||||
|
{value && (
|
||||||
|
<X
|
||||||
|
className="h-3.5 w-3.5 opacity-50 hover:opacity-100 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onChange("");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ChevronsUpDown className="h-3.5 w-3.5 opacity-50" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
sideOffset={6}
|
||||||
|
avoidCollisions={true}
|
||||||
|
collisionPadding={8}
|
||||||
|
className="z-[1000] w-[var(--radix-popover-trigger-width)] p-0 border-border-default"
|
||||||
|
>
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder={t("omo.searchModel", {
|
||||||
|
defaultValue: "Search model...",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>
|
||||||
|
{t("omo.noEnabledModels", {
|
||||||
|
defaultValue: "No configured models",
|
||||||
|
})}
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{options.map((option) => (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
keywords={[option.label]}
|
||||||
|
onSelect={() => {
|
||||||
|
onChange(option.value);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
value === option.value ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{option.label}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getAdvancedStr(config: Record<string, unknown> | undefined): string {
|
function getAdvancedStr(config: Record<string, unknown> | undefined): string {
|
||||||
if (!config) return "";
|
if (!config) return "";
|
||||||
@@ -86,24 +240,58 @@ function collectCustomModels(
|
|||||||
customs.push({
|
customs.push({
|
||||||
key: k,
|
key: k,
|
||||||
model: ((v as Record<string, unknown>).model as string) || "",
|
model: ((v as Record<string, unknown>).model as string) || "",
|
||||||
|
sourceKey: k,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return customs;
|
return customs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeCustomModelsIntoStore(
|
export function mergeCustomModelsIntoStore(
|
||||||
store: Record<string, Record<string, unknown>>,
|
store: Record<string, Record<string, unknown>>,
|
||||||
builtinKeys: Set<string>,
|
builtinKeys: Set<string>,
|
||||||
customs: CustomModelItem[],
|
customs: CustomModelItem[],
|
||||||
|
modelVariantsMap: Record<string, string[]>,
|
||||||
): Record<string, Record<string, unknown>> {
|
): Record<string, Record<string, unknown>> {
|
||||||
const updated = { ...store };
|
const updated: Record<string, Record<string, unknown>> = {};
|
||||||
for (const key of Object.keys(updated)) {
|
|
||||||
if (!builtinKeys.has(key)) delete updated[key];
|
for (const [key, value] of Object.entries(store)) {
|
||||||
|
if (builtinKeys.has(key)) {
|
||||||
|
updated[key] = { ...value };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const custom of customs) {
|
for (const custom of customs) {
|
||||||
if (custom.key.trim()) {
|
const targetKey = custom.key.trim();
|
||||||
updated[custom.key] = { ...updated[custom.key], model: custom.model };
|
if (!targetKey) continue;
|
||||||
|
|
||||||
|
const sourceKey = (custom.sourceKey || targetKey).trim();
|
||||||
|
const sourceEntry = store[sourceKey] ?? store[targetKey];
|
||||||
|
const nextEntry = {
|
||||||
|
...(updated[targetKey] || {}),
|
||||||
|
...(sourceEntry || {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (custom.model.trim()) {
|
||||||
|
nextEntry.model = custom.model;
|
||||||
|
const currentVariant =
|
||||||
|
typeof nextEntry.variant === "string" ? nextEntry.variant : "";
|
||||||
|
if (currentVariant) {
|
||||||
|
const validVariants = modelVariantsMap[custom.model] || [];
|
||||||
|
if (!validVariants.includes(currentVariant)) {
|
||||||
|
delete nextEntry.variant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updated[targetKey] = nextEntry;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete nextEntry.model;
|
||||||
|
delete nextEntry.variant;
|
||||||
|
if (Object.keys(nextEntry).length > 0) {
|
||||||
|
updated[targetKey] = nextEntry;
|
||||||
|
} else {
|
||||||
|
delete updated[targetKey];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
@@ -112,6 +300,7 @@ function mergeCustomModelsIntoStore(
|
|||||||
export function OmoFormFields({
|
export function OmoFormFields({
|
||||||
modelOptions,
|
modelOptions,
|
||||||
modelVariantsMap = {},
|
modelVariantsMap = {},
|
||||||
|
presetMetaMap: _presetMetaMap = {},
|
||||||
agents,
|
agents,
|
||||||
onAgentsChange,
|
onAgentsChange,
|
||||||
categories,
|
categories,
|
||||||
@@ -119,8 +308,7 @@ export function OmoFormFields({
|
|||||||
otherFieldsStr,
|
otherFieldsStr,
|
||||||
onOtherFieldsStrChange,
|
onOtherFieldsStrChange,
|
||||||
}: OmoFormFieldsProps) {
|
}: OmoFormFieldsProps) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isZh = i18n.language?.startsWith("zh");
|
|
||||||
|
|
||||||
const [mainAgentsOpen, setMainAgentsOpen] = useState(true);
|
const [mainAgentsOpen, setMainAgentsOpen] = useState(true);
|
||||||
const [subAgentsOpen, setSubAgentsOpen] = useState(true);
|
const [subAgentsOpen, setSubAgentsOpen] = useState(true);
|
||||||
@@ -159,19 +347,29 @@ export function OmoFormFields({
|
|||||||
const syncCustomAgents = useCallback(
|
const syncCustomAgents = useCallback(
|
||||||
(customs: CustomModelItem[]) => {
|
(customs: CustomModelItem[]) => {
|
||||||
onAgentsChange(
|
onAgentsChange(
|
||||||
mergeCustomModelsIntoStore(agents, BUILTIN_AGENT_KEYS, customs),
|
mergeCustomModelsIntoStore(
|
||||||
|
agents,
|
||||||
|
BUILTIN_AGENT_KEYS,
|
||||||
|
customs,
|
||||||
|
modelVariantsMap,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[agents, onAgentsChange],
|
[agents, onAgentsChange, modelVariantsMap],
|
||||||
);
|
);
|
||||||
|
|
||||||
const syncCustomCategories = useCallback(
|
const syncCustomCategories = useCallback(
|
||||||
(customs: CustomModelItem[]) => {
|
(customs: CustomModelItem[]) => {
|
||||||
onCategoriesChange(
|
onCategoriesChange(
|
||||||
mergeCustomModelsIntoStore(categories, BUILTIN_CATEGORY_KEYS, customs),
|
mergeCustomModelsIntoStore(
|
||||||
|
categories,
|
||||||
|
BUILTIN_CATEGORY_KEYS,
|
||||||
|
customs,
|
||||||
|
modelVariantsMap,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[categories, onCategoriesChange],
|
[categories, onCategoriesChange, modelVariantsMap],
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildEffectiveModelOptions = useCallback(
|
const buildEffectiveModelOptions = useCallback(
|
||||||
@@ -212,43 +410,16 @@ export function OmoFormFields({
|
|||||||
const renderModelSelect = (
|
const renderModelSelect = (
|
||||||
currentModel: string,
|
currentModel: string,
|
||||||
onChange: (value: string) => void,
|
onChange: (value: string) => void,
|
||||||
placeholder?: string,
|
recommended?: string,
|
||||||
) => {
|
) => {
|
||||||
const options = buildEffectiveModelOptions(currentModel);
|
const options = buildEffectiveModelOptions(currentModel);
|
||||||
return (
|
return (
|
||||||
<Select
|
<ModelCombobox
|
||||||
value={currentModel || EMPTY_MODEL_VALUE}
|
value={currentModel}
|
||||||
onValueChange={(value) =>
|
options={options}
|
||||||
onChange(value === EMPTY_MODEL_VALUE ? "" : value)
|
recommended={recommended}
|
||||||
}
|
onChange={onChange}
|
||||||
>
|
/>
|
||||||
<SelectTrigger className="flex-1 h-8 text-sm">
|
|
||||||
<SelectValue
|
|
||||||
placeholder={
|
|
||||||
placeholder ||
|
|
||||||
t("omo.selectEnabledModel", {
|
|
||||||
defaultValue: "Select enabled model",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="max-h-72">
|
|
||||||
<SelectItem value={EMPTY_MODEL_VALUE}>
|
|
||||||
{t("omo.clearWrapped", { defaultValue: "(Clear)" })}
|
|
||||||
</SelectItem>
|
|
||||||
{options.length === 0 ? (
|
|
||||||
<SelectItem value={UNAVAILABLE_MODEL_VALUE} disabled>
|
|
||||||
{t("omo.noEnabledModels", { defaultValue: "No enabled models" })}
|
|
||||||
</SelectItem>
|
|
||||||
) : (
|
|
||||||
options.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -268,11 +439,21 @@ export function OmoFormFields({
|
|||||||
currentVariant: string,
|
currentVariant: string,
|
||||||
onChange: (value: string) => void,
|
onChange: (value: string) => void,
|
||||||
) => {
|
) => {
|
||||||
|
const hasModel = Boolean(currentModel);
|
||||||
|
const modelVariantKeys = hasModel
|
||||||
|
? modelVariantsMap[currentModel] || []
|
||||||
|
: [];
|
||||||
|
const hasVariants = modelVariantKeys.length > 0;
|
||||||
|
const shouldShow = hasModel && (hasVariants || Boolean(currentVariant));
|
||||||
|
|
||||||
|
if (!shouldShow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const variantOptions = buildEffectiveVariantOptions(
|
const variantOptions = buildEffectiveVariantOptions(
|
||||||
currentModel,
|
currentModel,
|
||||||
currentVariant,
|
currentVariant,
|
||||||
);
|
);
|
||||||
const hasModel = Boolean(currentModel);
|
|
||||||
const firstIsUnavailable =
|
const firstIsUnavailable =
|
||||||
Boolean(currentVariant) &&
|
Boolean(currentVariant) &&
|
||||||
!(modelVariantsMap[currentModel] || []).includes(currentVariant);
|
!(modelVariantsMap[currentModel] || []).includes(currentVariant);
|
||||||
@@ -283,9 +464,8 @@ export function OmoFormFields({
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
onChange(value === EMPTY_VARIANT_VALUE ? "" : value)
|
onChange(value === EMPTY_VARIANT_VALUE ? "" : value)
|
||||||
}
|
}
|
||||||
disabled={!hasModel}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-32 h-8 text-xs shrink-0">
|
<SelectTrigger className="w-28 h-8 text-xs shrink-0">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={t("omo.variantPlaceholder", {
|
placeholder={t("omo.variantPlaceholder", {
|
||||||
defaultValue: "variant",
|
defaultValue: "variant",
|
||||||
@@ -296,30 +476,16 @@ export function OmoFormFields({
|
|||||||
<SelectItem value={EMPTY_VARIANT_VALUE}>
|
<SelectItem value={EMPTY_VARIANT_VALUE}>
|
||||||
{t("omo.defaultWrapped", { defaultValue: "(Default)" })}
|
{t("omo.defaultWrapped", { defaultValue: "(Default)" })}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{!hasModel ? (
|
{variantOptions.map((variant, index) => (
|
||||||
<SelectItem value={UNAVAILABLE_VARIANT_VALUE} disabled>
|
<SelectItem key={`${variant}-${index}`} value={variant}>
|
||||||
{t("omo.selectModelFirst", {
|
{firstIsUnavailable && index === 0
|
||||||
defaultValue: "Select model first",
|
? t("omo.currentValueUnavailable", {
|
||||||
})}
|
value: variant,
|
||||||
|
defaultValue: "{{value}} (current value, unavailable)",
|
||||||
|
})
|
||||||
|
: variant}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
) : variantOptions.length === 0 ? (
|
))}
|
||||||
<SelectItem value={UNAVAILABLE_VARIANT_VALUE} disabled>
|
|
||||||
{t("omo.noVariantsForModel", {
|
|
||||||
defaultValue: "No variants for model",
|
|
||||||
})}
|
|
||||||
</SelectItem>
|
|
||||||
) : (
|
|
||||||
variantOptions.map((variant, index) => (
|
|
||||||
<SelectItem key={`${variant}-${index}`} value={variant}>
|
|
||||||
{firstIsUnavailable && index === 0
|
|
||||||
? t("omo.currentValueUnavailable", {
|
|
||||||
value: variant,
|
|
||||||
defaultValue: "{{value}} (current value, unavailable)",
|
|
||||||
})
|
|
||||||
: variant}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
@@ -536,7 +702,7 @@ export function OmoFormFields({
|
|||||||
toast.warning(
|
toast.warning(
|
||||||
t("omo.noEnabledModelsWarning", {
|
t("omo.noEnabledModelsWarning", {
|
||||||
defaultValue:
|
defaultValue:
|
||||||
"No enabled models available. Configure and enable OpenCode models first.",
|
"No configured models available. Configure OpenCode models first.",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -641,9 +807,17 @@ export function OmoFormFields({
|
|||||||
<div key={key} className="border-b border-border/30 last:border-b-0">
|
<div key={key} className="border-b border-border/30 last:border-b-0">
|
||||||
<div className="flex items-center gap-2 py-1.5">
|
<div className="flex items-center gap-2 py-1.5">
|
||||||
<div className="w-32 shrink-0">
|
<div className="w-32 shrink-0">
|
||||||
<div className="text-sm font-medium">{def.display}</div>
|
<div className="flex items-center gap-1 text-sm font-medium">
|
||||||
|
{def.display}
|
||||||
|
<span className="relative inline-flex group/tip">
|
||||||
|
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground/60 hover:text-muted-foreground cursor-help shrink-0" />
|
||||||
|
<span className="invisible opacity-0 group-hover/tip:visible group-hover/tip:opacity-100 transition-opacity duration-150 absolute left-0 top-full mt-1 z-50 w-[260px] rounded-md bg-popover text-popover-foreground border border-border shadow-md px-3 py-2 text-xs leading-relaxed font-normal pointer-events-none">
|
||||||
|
{t(def.tooltipKey)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="text-xs text-muted-foreground truncate">
|
<div className="text-xs text-muted-foreground truncate">
|
||||||
{isZh ? def.descZh : def.descEn}
|
{t(def.descKey)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{renderModelSelect(
|
{renderModelSelect(
|
||||||
@@ -727,16 +901,14 @@ export function OmoFormFields({
|
|||||||
className="border-b border-border/30 last:border-b-0"
|
className="border-b border-border/30 last:border-b-0"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 py-1.5">
|
<div className="flex items-center gap-2 py-1.5">
|
||||||
<Input
|
<DeferredKeyInput
|
||||||
value={item.key}
|
value={item.key}
|
||||||
onChange={(e) => updateCustom({ key: e.target.value })}
|
onCommit={(value) => updateCustom({ key: value })}
|
||||||
placeholder={keyPlaceholder}
|
placeholder={keyPlaceholder}
|
||||||
className="w-32 shrink-0 h-8 text-sm text-primary"
|
className="w-32 shrink-0 h-8 text-sm text-primary"
|
||||||
/>
|
/>
|
||||||
{renderModelSelect(
|
{renderModelSelect(item.model, (value) =>
|
||||||
item.model,
|
updateCustom({ model: value }),
|
||||||
(value) => updateCustom({ model: value }),
|
|
||||||
t("omo.modelNamePlaceholder", { defaultValue: "model-name" }),
|
|
||||||
)}
|
)}
|
||||||
{renderVariantSelect(item.model, currentVariant, (value) => {
|
{renderVariantSelect(item.model, currentVariant, (value) => {
|
||||||
if (!item.key) return;
|
if (!item.key) return;
|
||||||
@@ -877,11 +1049,17 @@ export function OmoFormFields({
|
|||||||
|
|
||||||
const addCustomModel = (scope: AdvancedScope) => {
|
const addCustomModel = (scope: AdvancedScope) => {
|
||||||
if (scope === "agent") {
|
if (scope === "agent") {
|
||||||
setCustomAgents((prev) => [...prev, { key: "", model: "" }]);
|
setCustomAgents((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ key: "", model: "", sourceKey: "" },
|
||||||
|
]);
|
||||||
setSubAgentsOpen(true);
|
setSubAgentsOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCustomCategories((prev) => [...prev, { key: "", model: "" }]);
|
setCustomCategories((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ key: "", model: "", sourceKey: "" },
|
||||||
|
]);
|
||||||
setCategoriesOpen(true);
|
setCategoriesOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -931,7 +1109,7 @@ export function OmoFormFields({
|
|||||||
·{" "}
|
·{" "}
|
||||||
{t("omo.enabledModelsCount", {
|
{t("omo.enabledModelsCount", {
|
||||||
count: modelOptions.length,
|
count: modelOptions.length,
|
||||||
defaultValue: "{{count}} enabled models available",
|
defaultValue: "{{count}} configured models available",
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
{localFilePath && (
|
{localFilePath && (
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from "@/config/geminiProviderPresets";
|
} from "@/config/geminiProviderPresets";
|
||||||
import {
|
import {
|
||||||
opencodeProviderPresets,
|
opencodeProviderPresets,
|
||||||
|
OPENCODE_PRESET_MODEL_VARIANTS,
|
||||||
type OpenCodeProviderPreset,
|
type OpenCodeProviderPreset,
|
||||||
} from "@/config/opencodeProviderPresets";
|
} from "@/config/opencodeProviderPresets";
|
||||||
import { OpenCodeFormFields } from "./OpenCodeFormFields";
|
import { OpenCodeFormFields } from "./OpenCodeFormFields";
|
||||||
@@ -53,7 +54,7 @@ import { type OmoGlobalConfigFieldsRef } from "./OmoGlobalConfigFields";
|
|||||||
import { OmoCommonConfigEditor } from "./OmoCommonConfigEditor";
|
import { OmoCommonConfigEditor } from "./OmoCommonConfigEditor";
|
||||||
import * as configApi from "@/lib/api/config";
|
import * as configApi from "@/lib/api/config";
|
||||||
import type { OmoGlobalConfig } from "@/types/omo";
|
import type { OmoGlobalConfig } from "@/types/omo";
|
||||||
import { mergeOmoConfigPreview } from "@/types/omo";
|
import { mergeOmoConfigPreview, parseOmoOtherFieldsObject } from "@/types/omo";
|
||||||
import {
|
import {
|
||||||
ProviderAdvancedConfig,
|
ProviderAdvancedConfig,
|
||||||
type PricingModelSourceOption,
|
type PricingModelSourceOption,
|
||||||
@@ -112,21 +113,25 @@ const isKnownOpencodeOptionKey = (key: string) =>
|
|||||||
function parseOpencodeConfig(
|
function parseOpencodeConfig(
|
||||||
settingsConfig?: Record<string, unknown>,
|
settingsConfig?: Record<string, unknown>,
|
||||||
): OpenCodeProviderConfig {
|
): OpenCodeProviderConfig {
|
||||||
|
const normalize = (
|
||||||
|
parsed: Partial<OpenCodeProviderConfig>,
|
||||||
|
): OpenCodeProviderConfig => ({
|
||||||
|
npm: parsed.npm || OPENCODE_DEFAULT_NPM,
|
||||||
|
options:
|
||||||
|
parsed.options && typeof parsed.options === "object"
|
||||||
|
? (parsed.options as OpenCodeProviderConfig["options"])
|
||||||
|
: {},
|
||||||
|
models:
|
||||||
|
parsed.models && typeof parsed.models === "object"
|
||||||
|
? (parsed.models as Record<string, OpenCodeModel>)
|
||||||
|
: {},
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(
|
const parsed = JSON.parse(
|
||||||
settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG,
|
settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG,
|
||||||
) as Partial<OpenCodeProviderConfig>;
|
) as Partial<OpenCodeProviderConfig>;
|
||||||
return {
|
return normalize(parsed);
|
||||||
npm: parsed.npm || OPENCODE_DEFAULT_NPM,
|
|
||||||
options:
|
|
||||||
parsed.options && typeof parsed.options === "object"
|
|
||||||
? (parsed.options as OpenCodeProviderConfig["options"])
|
|
||||||
: {},
|
|
||||||
models:
|
|
||||||
parsed.models && typeof parsed.models === "object"
|
|
||||||
? (parsed.models as Record<string, OpenCodeModel>)
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
npm: OPENCODE_DEFAULT_NPM,
|
npm: OPENCODE_DEFAULT_NPM,
|
||||||
@@ -136,6 +141,25 @@ function parseOpencodeConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseOpencodeConfigStrict(
|
||||||
|
settingsConfig?: Record<string, unknown>,
|
||||||
|
): OpenCodeProviderConfig {
|
||||||
|
const parsed = JSON.parse(
|
||||||
|
settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG,
|
||||||
|
) as Partial<OpenCodeProviderConfig>;
|
||||||
|
return {
|
||||||
|
npm: parsed.npm || OPENCODE_DEFAULT_NPM,
|
||||||
|
options:
|
||||||
|
parsed.options && typeof parsed.options === "object"
|
||||||
|
? (parsed.options as OpenCodeProviderConfig["options"])
|
||||||
|
: {},
|
||||||
|
models:
|
||||||
|
parsed.models && typeof parsed.models === "object"
|
||||||
|
? (parsed.models as Record<string, OpenCodeModel>)
|
||||||
|
: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function toOpencodeExtraOptions(
|
function toOpencodeExtraOptions(
|
||||||
options: OpenCodeProviderConfig["options"],
|
options: OpenCodeProviderConfig["options"],
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
@@ -195,8 +219,10 @@ function buildOmoProfilePreview(
|
|||||||
}
|
}
|
||||||
if (otherFieldsStr.trim()) {
|
if (otherFieldsStr.trim()) {
|
||||||
try {
|
try {
|
||||||
const other = JSON.parse(otherFieldsStr);
|
const other = parseOmoOtherFieldsObject(otherFieldsStr);
|
||||||
Object.assign(profileOnly, other);
|
if (other) {
|
||||||
|
Object.assign(profileOnly, other);
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
return profileOnly;
|
return profileOnly;
|
||||||
@@ -580,19 +606,24 @@ export function ProviderForm({
|
|||||||
);
|
);
|
||||||
}, [opencodeProvidersData?.providers, providerId]);
|
}, [opencodeProvidersData?.providers, providerId]);
|
||||||
const [enabledOpencodeProviderIds, setEnabledOpencodeProviderIds] = useState<
|
const [enabledOpencodeProviderIds, setEnabledOpencodeProviderIds] = useState<
|
||||||
string[]
|
string[] | null
|
||||||
>([]);
|
>(null);
|
||||||
|
const [omoLiveIdsLoadFailed, setOmoLiveIdsLoadFailed] = useState(false);
|
||||||
|
const lastOmoModelSourceWarningRef = useRef<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
|
|
||||||
if (!isOmoCategory) {
|
if (!isOmoCategory) {
|
||||||
setEnabledOpencodeProviderIds([]);
|
setEnabledOpencodeProviderIds(null);
|
||||||
|
setOmoLiveIdsLoadFailed(false);
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEnabledOpencodeProviderIds(null);
|
||||||
|
setOmoLiveIdsLoadFailed(false);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const ids = await providersApi.getOpenCodeLiveProviderIds();
|
const ids = await providersApi.getOpenCodeLiveProviderIds();
|
||||||
@@ -600,9 +631,13 @@ export function ProviderForm({
|
|||||||
setEnabledOpencodeProviderIds(ids);
|
setEnabledOpencodeProviderIds(ids);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load OpenCode live provider ids:", error);
|
console.warn(
|
||||||
|
"[OMO_MODEL_SOURCE_LIVE_IDS_FAILED] failed to load live provider ids",
|
||||||
|
error,
|
||||||
|
);
|
||||||
if (active) {
|
if (active) {
|
||||||
setEnabledOpencodeProviderIds([]);
|
setOmoLiveIdsLoadFailed(true);
|
||||||
|
setEnabledOpencodeProviderIds(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -612,23 +647,71 @@ export function ProviderForm({
|
|||||||
};
|
};
|
||||||
}, [isOmoCategory]);
|
}, [isOmoCategory]);
|
||||||
|
|
||||||
const omoModelOptions = useMemo(() => {
|
const omoModelBuild = useMemo(() => {
|
||||||
if (!isOmoCategory) return [];
|
const empty = {
|
||||||
|
options: [] as Array<{ value: string; label: string }>,
|
||||||
|
variantsMap: {} as Record<string, string[]>,
|
||||||
|
presetMetaMap: {} as Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
options?: Record<string, unknown>;
|
||||||
|
limit?: { context?: number; output?: number };
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
parseFailedProviders: [] as string[],
|
||||||
|
usedFallbackSource: false,
|
||||||
|
};
|
||||||
|
if (!isOmoCategory) {
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
const allProviders = opencodeProvidersData?.providers;
|
const allProviders = opencodeProvidersData?.providers;
|
||||||
if (!allProviders) return [];
|
if (!allProviders) {
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
const enabledSet = new Set(enabledOpencodeProviderIds);
|
const shouldFilterByLive = !omoLiveIdsLoadFailed;
|
||||||
if (enabledSet.size === 0) return [];
|
if (shouldFilterByLive && enabledOpencodeProviderIds === null) {
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
const liveSet =
|
||||||
|
shouldFilterByLive && enabledOpencodeProviderIds
|
||||||
|
? new Set(enabledOpencodeProviderIds)
|
||||||
|
: null;
|
||||||
|
|
||||||
const dedupedOptions = new Map<string, string>();
|
const dedupedOptions = new Map<string, string>();
|
||||||
|
const variantsMap: Record<string, string[]> = {};
|
||||||
|
const presetMetaMap: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
options?: Record<string, unknown>;
|
||||||
|
limit?: { context?: number; output?: number };
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
const parseFailedProviders: string[] = [];
|
||||||
|
|
||||||
for (const [providerKey, provider] of Object.entries(allProviders)) {
|
for (const [providerKey, provider] of Object.entries(allProviders)) {
|
||||||
if (provider.category === "omo" || !enabledSet.has(providerKey)) {
|
if (provider.category === "omo") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (liveSet && !liveSet.has(providerKey)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedConfig = parseOpencodeConfig(provider.settingsConfig);
|
let parsedConfig: OpenCodeProviderConfig;
|
||||||
|
try {
|
||||||
|
parsedConfig = parseOpencodeConfigStrict(provider.settingsConfig);
|
||||||
|
} catch (error) {
|
||||||
|
parseFailedProviders.push(providerKey);
|
||||||
|
console.warn(
|
||||||
|
"[OMO_MODEL_SOURCE_PARSE_FAILED] failed to parse provider settings",
|
||||||
|
{
|
||||||
|
providerKey,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (const [modelId, model] of Object.entries(
|
for (const [modelId, model] of Object.entries(
|
||||||
parsedConfig.models || {},
|
parsedConfig.models || {},
|
||||||
)) {
|
)) {
|
||||||
@@ -645,63 +728,107 @@ export function ProviderForm({
|
|||||||
if (!dedupedOptions.has(value)) {
|
if (!dedupedOptions.has(value)) {
|
||||||
dedupedOptions.set(value, label);
|
dedupedOptions.set(value, label);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(dedupedOptions.entries())
|
|
||||||
.map(([value, label]) => ({ value, label }))
|
|
||||||
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN"));
|
|
||||||
}, [
|
|
||||||
isOmoCategory,
|
|
||||||
opencodeProvidersData?.providers,
|
|
||||||
enabledOpencodeProviderIds,
|
|
||||||
]);
|
|
||||||
const omoModelVariantsMap = useMemo(() => {
|
|
||||||
const variantsMap: Record<string, string[]> = {};
|
|
||||||
if (!isOmoCategory) {
|
|
||||||
return variantsMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allProviders = opencodeProvidersData?.providers;
|
|
||||||
if (!allProviders) {
|
|
||||||
return variantsMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enabledSet = new Set(enabledOpencodeProviderIds);
|
|
||||||
if (enabledSet.size === 0) {
|
|
||||||
return variantsMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [providerKey, provider] of Object.entries(allProviders)) {
|
|
||||||
if (provider.category === "omo" || !enabledSet.has(providerKey)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedConfig = parseOpencodeConfig(provider.settingsConfig);
|
|
||||||
for (const [modelId, model] of Object.entries(
|
|
||||||
parsedConfig.models || {},
|
|
||||||
)) {
|
|
||||||
const rawVariants = model.variants;
|
const rawVariants = model.variants;
|
||||||
if (
|
if (
|
||||||
!rawVariants ||
|
rawVariants &&
|
||||||
typeof rawVariants !== "object" ||
|
typeof rawVariants === "object" &&
|
||||||
Array.isArray(rawVariants)
|
!Array.isArray(rawVariants)
|
||||||
) {
|
) {
|
||||||
continue;
|
const variantKeys = Object.keys(rawVariants).filter(Boolean);
|
||||||
|
if (variantKeys.length > 0) {
|
||||||
|
variantsMap[value] = variantKeys;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const variantKeys = Object.keys(rawVariants).filter(Boolean);
|
}
|
||||||
if (variantKeys.length === 0) {
|
|
||||||
continue;
|
// Preset fallback: for models without config-defined variants,
|
||||||
|
// check if the npm package has preset variant definitions.
|
||||||
|
// Also collect preset metadata (options, limit) for enrichment.
|
||||||
|
const presetModels = OPENCODE_PRESET_MODEL_VARIANTS[parsedConfig.npm];
|
||||||
|
if (presetModels) {
|
||||||
|
for (const modelId of Object.keys(parsedConfig.models || {})) {
|
||||||
|
const fullKey = `${providerKey}/${modelId}`;
|
||||||
|
const preset = presetModels.find((p) => p.id === modelId);
|
||||||
|
if (!preset) continue;
|
||||||
|
|
||||||
|
// Variant fallback
|
||||||
|
if (!variantsMap[fullKey] && preset.variants) {
|
||||||
|
const presetKeys = Object.keys(preset.variants).filter(Boolean);
|
||||||
|
if (presetKeys.length > 0) {
|
||||||
|
variantsMap[fullKey] = presetKeys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect preset metadata for model enrichment
|
||||||
|
const meta: (typeof presetMetaMap)[string] = {};
|
||||||
|
if (preset.options) meta.options = preset.options;
|
||||||
|
if (preset.contextLimit || preset.outputLimit) {
|
||||||
|
meta.limit = {};
|
||||||
|
if (preset.contextLimit) meta.limit.context = preset.contextLimit;
|
||||||
|
if (preset.outputLimit) meta.limit.output = preset.outputLimit;
|
||||||
|
}
|
||||||
|
if (Object.keys(meta).length > 0) {
|
||||||
|
presetMetaMap[fullKey] = meta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
variantsMap[`${providerKey}/${modelId}`] = variantKeys;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return variantsMap;
|
return {
|
||||||
|
options: Array.from(dedupedOptions.entries())
|
||||||
|
.map(([value, label]) => ({ value, label }))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN")),
|
||||||
|
variantsMap,
|
||||||
|
presetMetaMap,
|
||||||
|
parseFailedProviders,
|
||||||
|
usedFallbackSource: omoLiveIdsLoadFailed,
|
||||||
|
};
|
||||||
}, [
|
}, [
|
||||||
isOmoCategory,
|
isOmoCategory,
|
||||||
opencodeProvidersData?.providers,
|
opencodeProvidersData?.providers,
|
||||||
enabledOpencodeProviderIds,
|
enabledOpencodeProviderIds,
|
||||||
|
omoLiveIdsLoadFailed,
|
||||||
|
]);
|
||||||
|
const omoModelOptions = omoModelBuild.options;
|
||||||
|
const omoModelVariantsMap = omoModelBuild.variantsMap;
|
||||||
|
const omoPresetMetaMap = omoModelBuild.presetMetaMap;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOmoCategory) return;
|
||||||
|
const failed = omoModelBuild.parseFailedProviders;
|
||||||
|
const fallback = omoModelBuild.usedFallbackSource;
|
||||||
|
if (failed.length === 0 && !fallback) return;
|
||||||
|
|
||||||
|
const signature = `${fallback ? "fallback:" : ""}${failed
|
||||||
|
.slice()
|
||||||
|
.sort()
|
||||||
|
.join(",")}`;
|
||||||
|
if (lastOmoModelSourceWarningRef.current === signature) return;
|
||||||
|
lastOmoModelSourceWarningRef.current = signature;
|
||||||
|
|
||||||
|
if (failed.length > 0) {
|
||||||
|
toast.warning(
|
||||||
|
t("omo.modelSourcePartialWarning", {
|
||||||
|
count: failed.length,
|
||||||
|
defaultValue:
|
||||||
|
"Some provider model configs are invalid and were skipped.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (fallback) {
|
||||||
|
toast.warning(
|
||||||
|
t("omo.modelSourceFallbackWarning", {
|
||||||
|
defaultValue:
|
||||||
|
"Failed to load live provider state. Falling back to configured providers.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isOmoCategory,
|
||||||
|
omoModelBuild.parseFailedProviders,
|
||||||
|
omoModelBuild.usedFallbackSource,
|
||||||
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const initialOmoSettings =
|
const initialOmoSettings =
|
||||||
@@ -1078,7 +1205,19 @@ export function ProviderForm({
|
|||||||
}
|
}
|
||||||
if (omoOtherFieldsStr.trim()) {
|
if (omoOtherFieldsStr.trim()) {
|
||||||
try {
|
try {
|
||||||
omoConfig.otherFields = JSON.parse(omoOtherFieldsStr);
|
const otherFields = parseOmoOtherFieldsObject(omoOtherFieldsStr);
|
||||||
|
if (!otherFields) {
|
||||||
|
toast.error(
|
||||||
|
t("omo.jsonMustBeObject", {
|
||||||
|
field: t("omo.otherFields", {
|
||||||
|
defaultValue: "Other Config",
|
||||||
|
}),
|
||||||
|
defaultValue: "{{field}} must be a JSON object",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
omoConfig.otherFields = otherFields;
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(
|
toast.error(
|
||||||
t("omo.invalidJson", {
|
t("omo.invalidJson", {
|
||||||
@@ -1603,6 +1742,7 @@ export function ProviderForm({
|
|||||||
<OmoFormFields
|
<OmoFormFields
|
||||||
modelOptions={omoModelOptions}
|
modelOptions={omoModelOptions}
|
||||||
modelVariantsMap={omoModelVariantsMap}
|
modelVariantsMap={omoModelVariantsMap}
|
||||||
|
presetMetaMap={omoPresetMetaMap}
|
||||||
agents={omoAgents}
|
agents={omoAgents}
|
||||||
onAgentsChange={setOmoAgents}
|
onAgentsChange={setOmoAgents}
|
||||||
categories={omoCategories}
|
categories={omoCategories}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Command = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof CommandPrimitive>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Command.displayName = CommandPrimitive.displayName;
|
||||||
|
|
||||||
|
const CommandInput = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof CommandPrimitive.Input>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
className="flex items-center border-b px-3 focus-within:outline-none focus-within:ring-0"
|
||||||
|
cmdk-input-wrapper=""
|
||||||
|
>
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||||
|
|
||||||
|
const CommandList = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof CommandPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const CommandEmpty = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof CommandPrimitive.Empty>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||||
|
>((props, ref) => (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
ref={ref}
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||||
|
|
||||||
|
const CommandGroup = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof CommandPrimitive.Group>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||||
|
|
||||||
|
const CommandItem = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof CommandPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, align = "start", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 rounded-md border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
));
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||||
@@ -24,6 +24,384 @@ export const opencodeNpmPackages = [
|
|||||||
{ value: "@ai-sdk/google", label: "Google (Gemini)" },
|
{ value: "@ai-sdk/google", label: "Google (Gemini)" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export interface PresetModelVariant {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
contextLimit?: number;
|
||||||
|
outputLimit?: number;
|
||||||
|
modalities?: { input: string[]; output: string[] };
|
||||||
|
options?: Record<string, unknown>;
|
||||||
|
variants?: Record<string, Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OPENCODE_PRESET_MODEL_VARIANTS: Record<
|
||||||
|
string,
|
||||||
|
PresetModelVariant[]
|
||||||
|
> = {
|
||||||
|
"@ai-sdk/openai-compatible": [
|
||||||
|
{
|
||||||
|
id: "MiniMax-M2.1",
|
||||||
|
name: "MiniMax M2.1",
|
||||||
|
contextLimit: 204800,
|
||||||
|
outputLimit: 131072,
|
||||||
|
modalities: { input: ["text"], output: ["text"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "glm-4.7",
|
||||||
|
name: "GLM 4.7",
|
||||||
|
contextLimit: 204800,
|
||||||
|
outputLimit: 131072,
|
||||||
|
modalities: { input: ["text"], output: ["text"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kimi-k2.5",
|
||||||
|
name: "Kimi K2.5",
|
||||||
|
contextLimit: 262144,
|
||||||
|
outputLimit: 262144,
|
||||||
|
modalities: { input: ["text", "image", "video"], output: ["text"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@ai-sdk/google": [
|
||||||
|
{
|
||||||
|
id: "gemini-2.5-flash-lite",
|
||||||
|
name: "Gemini 2.5 Flash Lite",
|
||||||
|
contextLimit: 1048576,
|
||||||
|
outputLimit: 65536,
|
||||||
|
modalities: {
|
||||||
|
input: ["text", "image", "pdf", "video", "audio"],
|
||||||
|
output: ["text"],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
auto: {
|
||||||
|
thinkingConfig: { includeThoughts: true, thinkingBudget: -1 },
|
||||||
|
},
|
||||||
|
"no-thinking": { thinkingConfig: { thinkingBudget: 0 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gemini-3-flash-preview",
|
||||||
|
name: "Gemini 3 Flash Preview",
|
||||||
|
contextLimit: 1048576,
|
||||||
|
outputLimit: 65536,
|
||||||
|
modalities: {
|
||||||
|
input: ["text", "image", "pdf", "video", "audio"],
|
||||||
|
output: ["text"],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
minimal: {
|
||||||
|
thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" },
|
||||||
|
},
|
||||||
|
low: {
|
||||||
|
thinkingConfig: { includeThoughts: true, thinkingLevel: "low" },
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
thinkingConfig: { includeThoughts: true, thinkingLevel: "medium" },
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
thinkingConfig: { includeThoughts: true, thinkingLevel: "high" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gemini-3-pro-preview",
|
||||||
|
name: "Gemini 3 Pro Preview",
|
||||||
|
contextLimit: 1048576,
|
||||||
|
outputLimit: 65536,
|
||||||
|
modalities: {
|
||||||
|
input: ["text", "image", "pdf", "video", "audio"],
|
||||||
|
output: ["text"],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
low: {
|
||||||
|
thinkingConfig: { includeThoughts: true, thinkingLevel: "low" },
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
thinkingConfig: { includeThoughts: true, thinkingLevel: "high" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@ai-sdk/openai": [
|
||||||
|
{
|
||||||
|
id: "gpt-5",
|
||||||
|
name: "GPT-5",
|
||||||
|
contextLimit: 400000,
|
||||||
|
outputLimit: 128000,
|
||||||
|
modalities: { input: ["text", "image"], output: ["text"] },
|
||||||
|
variants: {
|
||||||
|
low: {
|
||||||
|
reasoningEffort: "low",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "low",
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "high",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gpt-5.1",
|
||||||
|
name: "GPT-5.1",
|
||||||
|
contextLimit: 400000,
|
||||||
|
outputLimit: 272000,
|
||||||
|
modalities: { input: ["text", "image"], output: ["text"] },
|
||||||
|
variants: {
|
||||||
|
low: {
|
||||||
|
reasoningEffort: "low",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "low",
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "high",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gpt-5.1-codex",
|
||||||
|
name: "GPT-5.1 Codex",
|
||||||
|
contextLimit: 400000,
|
||||||
|
outputLimit: 128000,
|
||||||
|
modalities: { input: ["text", "image"], output: ["text"] },
|
||||||
|
options: { include: ["reasoning.encrypted_content"], store: false },
|
||||||
|
variants: {
|
||||||
|
low: {
|
||||||
|
reasoningEffort: "low",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gpt-5.1-codex-max",
|
||||||
|
name: "GPT-5.1 Codex Max",
|
||||||
|
contextLimit: 400000,
|
||||||
|
outputLimit: 128000,
|
||||||
|
modalities: { input: ["text", "image"], output: ["text"] },
|
||||||
|
options: { include: ["reasoning.encrypted_content"], store: false },
|
||||||
|
variants: {
|
||||||
|
low: {
|
||||||
|
reasoningEffort: "low",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
xhigh: {
|
||||||
|
reasoningEffort: "xhigh",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gpt-5.2",
|
||||||
|
name: "GPT-5.2",
|
||||||
|
contextLimit: 400000,
|
||||||
|
outputLimit: 128000,
|
||||||
|
modalities: { input: ["text", "image"], output: ["text"] },
|
||||||
|
variants: {
|
||||||
|
low: {
|
||||||
|
reasoningEffort: "low",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
xhigh: {
|
||||||
|
reasoningEffort: "xhigh",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gpt-5.2-codex",
|
||||||
|
name: "GPT-5.2 Codex",
|
||||||
|
contextLimit: 400000,
|
||||||
|
outputLimit: 128000,
|
||||||
|
modalities: { input: ["text", "image"], output: ["text"] },
|
||||||
|
options: { include: ["reasoning.encrypted_content"], store: false },
|
||||||
|
variants: {
|
||||||
|
low: {
|
||||||
|
reasoningEffort: "low",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
xhigh: {
|
||||||
|
reasoningEffort: "xhigh",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gpt-5.3-codex",
|
||||||
|
name: "GPT-5.3 Codex",
|
||||||
|
contextLimit: 400000,
|
||||||
|
outputLimit: 128000,
|
||||||
|
modalities: { input: ["text", "image"], output: ["text"] },
|
||||||
|
options: { include: ["reasoning.encrypted_content"], store: false },
|
||||||
|
variants: {
|
||||||
|
low: {
|
||||||
|
reasoningEffort: "low",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
xhigh: {
|
||||||
|
reasoningEffort: "xhigh",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@ai-sdk/anthropic": [
|
||||||
|
{
|
||||||
|
id: "claude-sonnet-4-5-20250929",
|
||||||
|
name: "Claude Sonnet 4.5",
|
||||||
|
contextLimit: 200000,
|
||||||
|
outputLimit: 64000,
|
||||||
|
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||||
|
variants: {
|
||||||
|
low: { effort: "low" },
|
||||||
|
medium: { effort: "medium" },
|
||||||
|
high: { effort: "high" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "claude-opus-4-5-20251101",
|
||||||
|
name: "Claude Opus 4.5",
|
||||||
|
contextLimit: 200000,
|
||||||
|
outputLimit: 64000,
|
||||||
|
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||||
|
variants: {
|
||||||
|
low: { thinking: { budgetTokens: 5000, type: "enabled" } },
|
||||||
|
medium: { thinking: { budgetTokens: 13000, type: "enabled" } },
|
||||||
|
high: { thinking: { budgetTokens: 18000, type: "enabled" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "claude-opus-4-6",
|
||||||
|
name: "Claude Opus 4.6",
|
||||||
|
contextLimit: 1000000,
|
||||||
|
outputLimit: 128000,
|
||||||
|
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||||
|
variants: {
|
||||||
|
low: { effort: "low" },
|
||||||
|
medium: { effort: "medium" },
|
||||||
|
high: { effort: "high" },
|
||||||
|
max: { effort: "max" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "claude-haiku-4-5-20251001",
|
||||||
|
name: "Claude Haiku 4.5",
|
||||||
|
contextLimit: 200000,
|
||||||
|
outputLimit: 64000,
|
||||||
|
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gemini-claude-opus-4-5-thinking",
|
||||||
|
name: "Antigravity - Claude Opus 4.5",
|
||||||
|
contextLimit: 200000,
|
||||||
|
outputLimit: 64000,
|
||||||
|
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||||
|
variants: {
|
||||||
|
low: { effort: "low" },
|
||||||
|
medium: { effort: "medium" },
|
||||||
|
high: { effort: "high" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gemini-claude-sonnet-4-5-thinking",
|
||||||
|
name: "Antigravity - Claude Sonnet 4.5",
|
||||||
|
contextLimit: 200000,
|
||||||
|
outputLimit: 64000,
|
||||||
|
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||||
|
variants: {
|
||||||
|
low: { thinking: { budgetTokens: 5000, type: "enabled" } },
|
||||||
|
medium: { thinking: { budgetTokens: 13000, type: "enabled" } },
|
||||||
|
high: { thinking: { budgetTokens: 18000, type: "enabled" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up preset metadata for a model by npm package and model ID.
|
||||||
|
* Returns enrichment fields (options, limit, modalities) that can be
|
||||||
|
* merged into a model definition when the user's config doesn't already
|
||||||
|
* provide them.
|
||||||
|
*/
|
||||||
|
export function getPresetModelDefaults(
|
||||||
|
npm: string,
|
||||||
|
modelId: string,
|
||||||
|
): PresetModelVariant | undefined {
|
||||||
|
const models = OPENCODE_PRESET_MODEL_VARIANTS[npm];
|
||||||
|
if (!models) return undefined;
|
||||||
|
return models.find((m) => m.id === modelId);
|
||||||
|
}
|
||||||
|
|
||||||
export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||||
{
|
{
|
||||||
name: "DeepSeek",
|
name: "DeepSeek",
|
||||||
@@ -664,7 +1042,10 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
"gpt-5.2": { name: "GPT-5.2" },
|
"gpt-5.2": { name: "GPT-5.2" },
|
||||||
"gpt-5.2-codex": { name: "GPT-5.2 Codex" },
|
"gpt-5.2-codex": {
|
||||||
|
name: "GPT-5.2 Codex",
|
||||||
|
options: { include: ["reasoning.encrypted_content"], store: false },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "third_party",
|
category: "third_party",
|
||||||
|
|||||||
@@ -1572,16 +1572,21 @@
|
|||||||
"clearWrapped": "(Clear)",
|
"clearWrapped": "(Clear)",
|
||||||
"defaultWrapped": "(Default)",
|
"defaultWrapped": "(Default)",
|
||||||
"variantPlaceholder": "variant",
|
"variantPlaceholder": "variant",
|
||||||
"selectEnabledModel": "Select enabled model",
|
"selectEnabledModel": "Select configured model",
|
||||||
|
"selectModel": "Select configured model",
|
||||||
|
"recommendedHint": "Recommended: {{model}}",
|
||||||
|
"searchModel": "Search model...",
|
||||||
"selectModelFirst": "Select model first",
|
"selectModelFirst": "Select model first",
|
||||||
"noEnabledModels": "No enabled models",
|
"noEnabledModels": "No configured models",
|
||||||
"noVariantsForModel": "No variants for model",
|
"noVariantsForModel": "No variants for model",
|
||||||
"currentValueNotEnabled": "{{value}} (current value, not enabled)",
|
"currentValueNotEnabled": "{{value}} (current value, not configured)",
|
||||||
"currentValueUnavailable": "{{value}} (current value, unavailable)",
|
"currentValueUnavailable": "{{value}} (current value, unavailable)",
|
||||||
"advancedLabel": "Advanced",
|
"advancedLabel": "Advanced",
|
||||||
"advancedJsonInvalid": "Advanced JSON is invalid",
|
"advancedJsonInvalid": "Advanced JSON is invalid",
|
||||||
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission, etc. Leave empty for defaults",
|
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission, etc. Leave empty for defaults",
|
||||||
"noEnabledModelsWarning": "No enabled models available. Configure and enable OpenCode models first.",
|
"noEnabledModelsWarning": "No configured models available. Configure OpenCode models first.",
|
||||||
|
"modelSourcePartialWarning": "Some provider model configs are invalid and were skipped.",
|
||||||
|
"modelSourceFallbackWarning": "Failed to load live provider state. Falling back to configured providers.",
|
||||||
"importLocalReplaceSuccess": "Imported local file and replaced Agents/Categories/Other Fields",
|
"importLocalReplaceSuccess": "Imported local file and replaced Agents/Categories/Other Fields",
|
||||||
"importLocalFailed": "Failed to read local file: {{error}}",
|
"importLocalFailed": "Failed to read local file: {{error}}",
|
||||||
"agentKeyPlaceholder": "agent key",
|
"agentKeyPlaceholder": "agent key",
|
||||||
@@ -1592,7 +1597,7 @@
|
|||||||
"modelConfiguration": "Model Configuration",
|
"modelConfiguration": "Model Configuration",
|
||||||
"fillRecommended": "Fill Recommended",
|
"fillRecommended": "Fill Recommended",
|
||||||
"configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params",
|
"configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params",
|
||||||
"enabledModelsCount": "{{count}} enabled models available",
|
"enabledModelsCount": "{{count}} configured models available",
|
||||||
"source": "from:",
|
"source": "from:",
|
||||||
"otherFieldsJson": "Other Fields (JSON)",
|
"otherFieldsJson": "Other Fields (JSON)",
|
||||||
"searchOrType": "Search or type custom value...",
|
"searchOrType": "Search or type custom value...",
|
||||||
@@ -1618,6 +1623,52 @@
|
|||||||
"advancedExperimental": "Experimental Features",
|
"advancedExperimental": "Experimental Features",
|
||||||
"advancedBackgroundTask": "Background Tasks",
|
"advancedBackgroundTask": "Background Tasks",
|
||||||
"advancedBrowserAutomation": "Browser Automation",
|
"advancedBrowserAutomation": "Browser Automation",
|
||||||
"advancedClaudeCode": "Claude Code"
|
"advancedClaudeCode": "Claude Code",
|
||||||
|
"agentDesc": {
|
||||||
|
"sisyphus": "Main orchestrator",
|
||||||
|
"hephaestus": "Autonomous deep worker",
|
||||||
|
"prometheus": "Strategic planner",
|
||||||
|
"atlas": "Task manager",
|
||||||
|
"oracle": "Strategic advisor",
|
||||||
|
"librarian": "Multi-repo researcher",
|
||||||
|
"explore": "Fast code search",
|
||||||
|
"multimodalLooker": "Media analyzer",
|
||||||
|
"metis": "Pre-plan analysis advisor",
|
||||||
|
"momus": "Plan reviewer",
|
||||||
|
"sisyphusJunior": "Delegated task executor"
|
||||||
|
},
|
||||||
|
"agentTooltip": {
|
||||||
|
"sisyphus": "Main orchestrator responsible for task planning, delegation and parallel execution. Uses extended thinking (32k budget) and drives workflows through TODO lists to ensure task completion.",
|
||||||
|
"atlas": "Main orchestrator (holds TODO list) responsible for task distribution and coordination during execution phase. Delegates to specialized agents rather than completing all work directly.",
|
||||||
|
"prometheus": "Strategic planner that collects requirements through interview mode and creates detailed work plans. Can only read/write Markdown files in .sisyphus/ directory, never writes code directly.",
|
||||||
|
"hephaestus": "Autonomous deep worker (\"legitimate craftsman\") inspired by AmpCode deep mode. Goal-oriented execution that launches 2-5 explore/librarian agents in parallel for research before taking action.",
|
||||||
|
"oracle": "Architecture decision and debugging advisor. Read-only consulting agent providing excellent logical reasoning and deep analysis. Cannot write files or delegate tasks.",
|
||||||
|
"librarian": "Multi-repository analysis and documentation retrieval expert. Deeply understands codebases and provides evidence-based answers. Excels at finding official documentation and open-source implementation examples.",
|
||||||
|
"explore": "Fast codebase exploration and context grep expert. Uses lightweight models for high-speed search. The scout for understanding code structure.",
|
||||||
|
"multimodalLooker": "Visual content expert that analyzes PDFs, images, charts and other non-text media, extracting information and insights from them.",
|
||||||
|
"metis": "Plan consultant that performs pre-analysis before planning. Identifies hidden intents, ambiguities and AI failure points to prevent over-engineering.",
|
||||||
|
"momus": "Plan reviewer that validates plan clarity, verifiability and completeness with high precision. Rejects and requests revisions until plans are perfect.",
|
||||||
|
"sisyphusJunior": "Category-generated executor, automatically created via category parameters. Focuses on executing assigned tasks and cannot re-delegate, preventing infinite delegation loops."
|
||||||
|
},
|
||||||
|
"categoryDesc": {
|
||||||
|
"visualEngineering": "Visual/frontend engineering",
|
||||||
|
"ultrabrain": "Ultra thinking",
|
||||||
|
"deep": "Deep work",
|
||||||
|
"artistry": "Creative/artistic",
|
||||||
|
"quick": "Quick response",
|
||||||
|
"unspecifiedLow": "General low tier",
|
||||||
|
"unspecifiedHigh": "General high tier",
|
||||||
|
"writing": "Writing"
|
||||||
|
},
|
||||||
|
"categoryTooltip": {
|
||||||
|
"visualEngineering": "Frontend and visual engineering category for UI/UX design, styling, animation and interface implementation. Defaults to Gemini 3 Pro model.",
|
||||||
|
"ultrabrain": "Deep logical reasoning category for complex architectural decisions requiring extensive analysis. Defaults to GPT-5.3 Codex ultra-high reasoning variant.",
|
||||||
|
"deep": "Deep autonomous problem-solving category with goal-oriented execution. Conducts thorough research before action, suitable for difficult problems requiring deep understanding.",
|
||||||
|
"artistry": "Highly creative and artistic task category that inspires novel ideas and creative solutions. Defaults to Gemini 3 Pro max variant.",
|
||||||
|
"quick": "Lightweight task category for single-file modifications, typo fixes, and simple adjustments. Defaults to Claude Haiku 4.5 fast model.",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1553,16 +1553,21 @@
|
|||||||
"clearWrapped": "(クリア)",
|
"clearWrapped": "(クリア)",
|
||||||
"defaultWrapped": "(デフォルト)",
|
"defaultWrapped": "(デフォルト)",
|
||||||
"variantPlaceholder": "variant",
|
"variantPlaceholder": "variant",
|
||||||
"selectEnabledModel": "有効なモデルを選択",
|
"selectEnabledModel": "設定済みモデルを選択",
|
||||||
|
"selectModel": "設定済みモデルを選択",
|
||||||
|
"recommendedHint": "推奨: {{model}}",
|
||||||
|
"searchModel": "モデルを検索...",
|
||||||
"selectModelFirst": "先にモデルを選択",
|
"selectModelFirst": "先にモデルを選択",
|
||||||
"noEnabledModels": "有効なモデルがありません",
|
"noEnabledModels": "設定済みモデルがありません",
|
||||||
"noVariantsForModel": "このモデルには思考レベルがありません",
|
"noVariantsForModel": "このモデルには思考レベルがありません",
|
||||||
"currentValueNotEnabled": "{{value}} (現在値・未有効)",
|
"currentValueNotEnabled": "{{value}} (現在値・未設定)",
|
||||||
"currentValueUnavailable": "{{value}} (現在値・利用不可)",
|
"currentValueUnavailable": "{{value}} (現在値・利用不可)",
|
||||||
"advancedLabel": "詳細",
|
"advancedLabel": "詳細",
|
||||||
"advancedJsonInvalid": "詳細 JSON が不正です",
|
"advancedJsonInvalid": "詳細 JSON が不正です",
|
||||||
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission など。空欄でデフォルトを使用します",
|
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission など。空欄でデフォルトを使用します",
|
||||||
"noEnabledModelsWarning": "利用可能な有効モデルがありません。先に OpenCode モデルを有効化してください。",
|
"noEnabledModelsWarning": "利用可能な設定済みモデルがありません。先に OpenCode モデルを設定してください。",
|
||||||
|
"modelSourcePartialWarning": "一部プロバイダーのモデル設定が不正なため、候補から除外しました。",
|
||||||
|
"modelSourceFallbackWarning": "live プロバイダー状態の取得に失敗したため、設定済みプロバイダーへフォールバックしました。",
|
||||||
"importLocalReplaceSuccess": "ローカルファイルから読み込み、Agents/Categories/Other Fields を置き換えました",
|
"importLocalReplaceSuccess": "ローカルファイルから読み込み、Agents/Categories/Other Fields を置き換えました",
|
||||||
"importLocalFailed": "ローカルファイルの読み込みに失敗しました: {{error}}",
|
"importLocalFailed": "ローカルファイルの読み込みに失敗しました: {{error}}",
|
||||||
"agentKeyPlaceholder": "agent キー",
|
"agentKeyPlaceholder": "agent キー",
|
||||||
@@ -1573,7 +1578,7 @@
|
|||||||
"modelConfiguration": "モデル設定",
|
"modelConfiguration": "モデル設定",
|
||||||
"fillRecommended": "推奨を入力",
|
"fillRecommended": "推奨を入力",
|
||||||
"configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開",
|
"configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開",
|
||||||
"enabledModelsCount": "有効モデル {{count}} 件",
|
"enabledModelsCount": "設定済みモデル {{count}} 件",
|
||||||
"source": "出典:",
|
"source": "出典:",
|
||||||
"otherFieldsJson": "その他のフィールド (JSON)",
|
"otherFieldsJson": "その他のフィールド (JSON)",
|
||||||
"searchOrType": "検索またはカスタム値を入力...",
|
"searchOrType": "検索またはカスタム値を入力...",
|
||||||
@@ -1599,6 +1604,52 @@
|
|||||||
"advancedExperimental": "実験的機能",
|
"advancedExperimental": "実験的機能",
|
||||||
"advancedBackgroundTask": "バックグラウンドタスク",
|
"advancedBackgroundTask": "バックグラウンドタスク",
|
||||||
"advancedBrowserAutomation": "ブラウザ自動化",
|
"advancedBrowserAutomation": "ブラウザ自動化",
|
||||||
"advancedClaudeCode": "Claude Code"
|
"advancedClaudeCode": "Claude Code",
|
||||||
|
"agentDesc": {
|
||||||
|
"sisyphus": "メインオーケストレーター",
|
||||||
|
"hephaestus": "自律型ディープワーカー",
|
||||||
|
"prometheus": "戦略プランナー",
|
||||||
|
"atlas": "タスクマネージャー",
|
||||||
|
"oracle": "戦略アドバイザー",
|
||||||
|
"librarian": "マルチリポジトリ研究員",
|
||||||
|
"explore": "高速コード検索",
|
||||||
|
"multimodalLooker": "メディアアナライザー",
|
||||||
|
"metis": "計画前分析アドバイザー",
|
||||||
|
"momus": "プランレビュアー",
|
||||||
|
"sisyphusJunior": "委任タスクエグゼキューター"
|
||||||
|
},
|
||||||
|
"agentTooltip": {
|
||||||
|
"sisyphus": "メインオーケストレーター。タスクの計画、委任、並列実行を担当。拡張思考(32k バジェット)を使用し、TODO リストでワークフローを駆動してタスク完了を確保します。",
|
||||||
|
"atlas": "メインオーケストレーター(TODO リスト保持)。実行フェーズでのタスク配分と調整を担当。すべての作業を直接行うのではなく、専門エージェントに委任します。",
|
||||||
|
"prometheus": "戦略プランナー。インタビューモードで要件を収集し、詳細な作業計画を策定。.sisyphus/ ディレクトリ内の Markdown ファイルの読み書きのみ可能で、直接コードを書くことはありません。",
|
||||||
|
"hephaestus": "自律型ディープワーカー(「正当な職人」)。AmpCode ディープモードに着想を得た目標指向の実行を行い、行動前に 2-5 個の探索/ライブラリアンエージェントを並列起動して調査します。",
|
||||||
|
"oracle": "アーキテクチャ決定とデバッグのアドバイザー。読み取り専用のコンサルティングエージェントで、優れた論理的推論と深い分析を提供。ファイルの書き込みやタスクの委任はできません。",
|
||||||
|
"librarian": "マルチリポジトリ分析とドキュメント検索の専門家。コードベースを深く理解し、エビデンスに基づく回答を提供。公式ドキュメントやオープンソース実装例の検索に長けています。",
|
||||||
|
"explore": "高速コードベース探索とコンテキスト grep の専門家。軽量モデルを使用した高速検索で、コード構造を理解するための先鋒です。",
|
||||||
|
"multimodalLooker": "ビジュアルコンテンツの専門家。PDF、画像、グラフなどの非テキストメディアを分析し、情報とインサイトを抽出します。",
|
||||||
|
"metis": "プランコンサルタント。計画前に事前分析を行い、隠れた意図、曖昧な点、AI の失敗ポイントを特定して、過剰エンジニアリングを防止します。",
|
||||||
|
"momus": "プランレビュアー。計画の明確さ、検証可能性、完全性を高精度で検証。計画が完璧になるまで却下と修正を要求します。",
|
||||||
|
"sisyphusJunior": "カテゴリ生成のエグゼキューター。category パラメータにより自動生成され、割り当てられたタスクの実行に専念し、再委任はできません。無限委任ループを防止します。"
|
||||||
|
},
|
||||||
|
"categoryDesc": {
|
||||||
|
"visualEngineering": "ビジュアル/フロントエンド工学",
|
||||||
|
"ultrabrain": "超深度思考",
|
||||||
|
"deep": "ディープワーク",
|
||||||
|
"artistry": "クリエイティブ/芸術",
|
||||||
|
"quick": "クイックレスポンス",
|
||||||
|
"unspecifiedLow": "汎用ロースペック",
|
||||||
|
"unspecifiedHigh": "汎用ハイスペック",
|
||||||
|
"writing": "ライティング"
|
||||||
|
},
|
||||||
|
"categoryTooltip": {
|
||||||
|
"visualEngineering": "フロントエンドとビジュアルエンジニアリングカテゴリ。UI/UX デザイン、スタイリング、アニメーション、インターフェース実装に特化。デフォルトで Gemini 3 Pro モデルを使用。",
|
||||||
|
"ultrabrain": "ディープロジック推論カテゴリ。広範な分析が必要な複雑なアーキテクチャ決定に使用。デフォルトで GPT-5.3 Codex の超高推論バリアントを使用。",
|
||||||
|
"deep": "ディープ自律問題解決カテゴリ。目標指向の実行で、行動前に徹底的な調査を実施。深い理解が必要な難問に適しています。",
|
||||||
|
"artistry": "高度にクリエイティブで芸術的なタスクカテゴリ。斬新なアイデアとクリエイティブなソリューションを促進。デフォルトで Gemini 3 Pro の max バリアントを使用。",
|
||||||
|
"quick": "軽量タスクカテゴリ。単一ファイルの修正、タイポ修正、簡単な調整などの些細な作業に使用。デフォルトで Claude Haiku 4.5 高速モデルを使用。",
|
||||||
|
"unspecifiedLow": "未分類の低作業量タスクカテゴリ。他のカテゴリに該当せず作業量が小さいタスクに適用。デフォルトで Claude Sonnet 4.5 を使用。",
|
||||||
|
"unspecifiedHigh": "未分類の高作業量タスクカテゴリ。他のカテゴリに該当せず作業量が大きいタスクに適用。デフォルトで Claude Opus 4.6 の max バリアントを使用。",
|
||||||
|
"writing": "ライティングカテゴリ。ドキュメント、散文、技術文書に特化。デフォルトで Gemini 3 Flash 高速生成モデルを使用。"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1572,16 +1572,21 @@
|
|||||||
"clearWrapped": "(清空)",
|
"clearWrapped": "(清空)",
|
||||||
"defaultWrapped": "(默认)",
|
"defaultWrapped": "(默认)",
|
||||||
"variantPlaceholder": "思考等级",
|
"variantPlaceholder": "思考等级",
|
||||||
"selectEnabledModel": "选择已启用模型",
|
"selectEnabledModel": "选择已配置模型",
|
||||||
|
"selectModel": "选择已配置模型",
|
||||||
|
"recommendedHint": "推荐: {{model}}",
|
||||||
|
"searchModel": "搜索模型...",
|
||||||
"selectModelFirst": "先选择模型",
|
"selectModelFirst": "先选择模型",
|
||||||
"noEnabledModels": "暂无已启用模型",
|
"noEnabledModels": "暂无已配置模型",
|
||||||
"noVariantsForModel": "该模型无思考等级",
|
"noVariantsForModel": "该模型无思考等级",
|
||||||
"currentValueNotEnabled": "{{value}}(当前值,未启用)",
|
"currentValueNotEnabled": "{{value}}(当前值,未配置)",
|
||||||
"currentValueUnavailable": "{{value}}(当前值,未启用)",
|
"currentValueUnavailable": "{{value}}(当前值,不可用)",
|
||||||
"advancedLabel": "高级参数",
|
"advancedLabel": "高级参数",
|
||||||
"advancedJsonInvalid": "高级参数 JSON 无效",
|
"advancedJsonInvalid": "高级参数 JSON 无效",
|
||||||
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission 等,留空使用默认值",
|
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission 等,留空使用默认值",
|
||||||
"noEnabledModelsWarning": "当前没有可用的已启用模型,请先启用并配置 OpenCode 模型",
|
"noEnabledModelsWarning": "当前没有可用的已配置模型,请先配置 OpenCode 模型",
|
||||||
|
"modelSourcePartialWarning": "部分供应商模型配置无效,已自动跳过。",
|
||||||
|
"modelSourceFallbackWarning": "读取 live 供应商状态失败,已回退到已配置供应商列表。",
|
||||||
"importLocalReplaceSuccess": "已从本地文件导入并覆盖 Agent/Category/Other Fields",
|
"importLocalReplaceSuccess": "已从本地文件导入并覆盖 Agent/Category/Other Fields",
|
||||||
"importLocalFailed": "读取本地文件失败: {{error}}",
|
"importLocalFailed": "读取本地文件失败: {{error}}",
|
||||||
"agentKeyPlaceholder": "agent 键名",
|
"agentKeyPlaceholder": "agent 键名",
|
||||||
@@ -1592,7 +1597,7 @@
|
|||||||
"modelConfiguration": "模型配置",
|
"modelConfiguration": "模型配置",
|
||||||
"fillRecommended": "填充推荐",
|
"fillRecommended": "填充推荐",
|
||||||
"configSummary": "已配置 {{agents}} 个 Agent,{{categories}} 个 Category · 点击 ⚙ 展开高级参数",
|
"configSummary": "已配置 {{agents}} 个 Agent,{{categories}} 个 Category · 点击 ⚙ 展开高级参数",
|
||||||
"enabledModelsCount": "可选已启用模型 {{count}} 个",
|
"enabledModelsCount": "可选已配置模型 {{count}} 个",
|
||||||
"source": "来源:",
|
"source": "来源:",
|
||||||
"otherFieldsJson": "其他字段 (JSON)",
|
"otherFieldsJson": "其他字段 (JSON)",
|
||||||
"searchOrType": "搜索或输入自定义值...",
|
"searchOrType": "搜索或输入自定义值...",
|
||||||
@@ -1618,6 +1623,52 @@
|
|||||||
"advancedExperimental": "实验性功能",
|
"advancedExperimental": "实验性功能",
|
||||||
"advancedBackgroundTask": "后台任务",
|
"advancedBackgroundTask": "后台任务",
|
||||||
"advancedBrowserAutomation": "浏览器自动化",
|
"advancedBrowserAutomation": "浏览器自动化",
|
||||||
"advancedClaudeCode": "Claude Code"
|
"advancedClaudeCode": "Claude Code",
|
||||||
|
"agentDesc": {
|
||||||
|
"sisyphus": "主编排者",
|
||||||
|
"hephaestus": "自主深度工作者",
|
||||||
|
"prometheus": "战略规划者",
|
||||||
|
"atlas": "任务管理者",
|
||||||
|
"oracle": "战略顾问",
|
||||||
|
"librarian": "多仓库研究员",
|
||||||
|
"explore": "快速代码搜索",
|
||||||
|
"multimodalLooker": "媒体分析器",
|
||||||
|
"metis": "规划前分析顾问",
|
||||||
|
"momus": "计划审查者",
|
||||||
|
"sisyphusJunior": "委托任务执行器"
|
||||||
|
},
|
||||||
|
"agentTooltip": {
|
||||||
|
"sisyphus": "主编排器,负责任务规划、委派与并行执行,使用扩展思考(32k 预算),通过 TODO 驱动工作流确保任务完成。",
|
||||||
|
"atlas": "主编排器(持有 TODO 列表),负责执行阶段的任务分发与协调,不直接完成所有工作而是委派给专业代理。",
|
||||||
|
"prometheus": "战略规划师,通过访谈模式收集需求并制定详细工作计划,仅能在 .sisyphus/ 目录读写 Markdown 文件,从不直接写代码。",
|
||||||
|
"hephaestus": "自主深度工作者(「合法工匠」),受 AmpCode 深度模式启发,目标导向执行,行动前会并行启动 2-5 个探索/图书管理员代理进行研究。",
|
||||||
|
"oracle": "架构决策与调试顾问,只读咨询代理,提供出色的逻辑推理和深度分析,不能写文件或委派任务。",
|
||||||
|
"librarian": "多仓库分析与文档检索专家,深度理解代码库并提供基于证据的答案,擅长查找官方文档和开源实现示例。",
|
||||||
|
"explore": "快速代码库探索与上下文 grep 专家,使用轻量级模型进行高速搜索,是理解代码结构的先锋。",
|
||||||
|
"multimodalLooker": "视觉内容专家,分析 PDF、图片、图表等非文本媒体,提取其中的信息与洞察。",
|
||||||
|
"metis": "计划顾问,在规划前进行预分析,识别隐藏意图、模糊点和 AI 失败点,防止过度工程化。",
|
||||||
|
"momus": "计划评审员,高精度验证计划的清晰度、可验证性和完整性,拒绝并要求修订直到计划完美。",
|
||||||
|
"sisyphusJunior": "类别生成的执行器,由 category 参数自动生成,专注于执行分配的任务且不能再委派,防止无限委派循环。"
|
||||||
|
},
|
||||||
|
"categoryDesc": {
|
||||||
|
"visualEngineering": "视觉/前端工程",
|
||||||
|
"ultrabrain": "超级思考",
|
||||||
|
"deep": "深度工作",
|
||||||
|
"artistry": "创意/文艺",
|
||||||
|
"quick": "快速响应",
|
||||||
|
"unspecifiedLow": "通用低配",
|
||||||
|
"unspecifiedHigh": "通用高配",
|
||||||
|
"writing": "写作"
|
||||||
|
},
|
||||||
|
"categoryTooltip": {
|
||||||
|
"visualEngineering": "前端与视觉工程类别,专注于 UI/UX 设计、样式、动画和界面实现,默认使用 Gemini 3 Pro 模型。",
|
||||||
|
"ultrabrain": "深度逻辑推理类别,用于需要广泛分析的复杂架构决策,默认使用 GPT-5.3 Codex 的超高推理变体。",
|
||||||
|
"deep": "深度自主问题解决类别,目标导向执行,行动前进行彻底研究,适用于需要深度理解的棘手问题。",
|
||||||
|
"artistry": "高度创意与艺术性任务类别,激发新颖想法和创造性解决方案,默认使用 Gemini 3 Pro 的最大变体。",
|
||||||
|
"quick": "轻量任务类别,用于单文件修改、错别字修复、简单调整等琐碎工作,默认使用 Claude Haiku 4.5 快速模型。",
|
||||||
|
"unspecifiedLow": "未归类低工作量任务类别,适用于不适合其他类别且工作量较小的任务,默认使用 Claude Sonnet 4.5。",
|
||||||
|
"unspecifiedHigh": "未归类高工作量任务类别,适用于不适合其他类别且工作量较大的任务,默认使用 Claude Opus 4.6 的最大变体。",
|
||||||
|
"writing": "写作类别,专注于文档、散文和技术写作,默认使用 Gemini 3 Flash 快速生成模型。"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-50
@@ -27,8 +27,8 @@ export interface OmoLocalFileData {
|
|||||||
export interface OmoAgentDef {
|
export interface OmoAgentDef {
|
||||||
key: string;
|
key: string;
|
||||||
display: string;
|
display: string;
|
||||||
descZh: string;
|
descKey: string;
|
||||||
descEn: string;
|
tooltipKey: string;
|
||||||
recommended?: string;
|
recommended?: string;
|
||||||
group: "main" | "sub";
|
group: "main" | "sub";
|
||||||
}
|
}
|
||||||
@@ -36,97 +36,97 @@ export interface OmoAgentDef {
|
|||||||
export interface OmoCategoryDef {
|
export interface OmoCategoryDef {
|
||||||
key: string;
|
key: string;
|
||||||
display: string;
|
display: string;
|
||||||
descZh: string;
|
descKey: string;
|
||||||
descEn: string;
|
tooltipKey: string;
|
||||||
recommended?: string;
|
recommended?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
|
export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
|
||||||
{
|
{
|
||||||
key: "Sisyphus",
|
key: "sisyphus",
|
||||||
display: "Sisyphus",
|
display: "Sisyphus",
|
||||||
descZh: "主编排者",
|
descKey: "omo.agentDesc.sisyphus",
|
||||||
descEn: "Main orchestrator",
|
tooltipKey: "omo.agentTooltip.sisyphus",
|
||||||
recommended: "claude-opus-4-6",
|
recommended: "claude-opus-4-6",
|
||||||
group: "main",
|
group: "main",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Hephaestus",
|
key: "hephaestus",
|
||||||
display: "Hephaestus",
|
display: "Hephaestus",
|
||||||
descZh: "自主深度工作者",
|
descKey: "omo.agentDesc.hephaestus",
|
||||||
descEn: "Autonomous deep worker",
|
tooltipKey: "omo.agentTooltip.hephaestus",
|
||||||
recommended: "gpt-5.3-codex",
|
recommended: "gpt-5.3-codex",
|
||||||
group: "main",
|
group: "main",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Prometheus",
|
key: "prometheus",
|
||||||
display: "Prometheus",
|
display: "Prometheus",
|
||||||
descZh: "战略规划者",
|
descKey: "omo.agentDesc.prometheus",
|
||||||
descEn: "Strategic planner",
|
tooltipKey: "omo.agentTooltip.prometheus",
|
||||||
recommended: "claude-opus-4-6",
|
recommended: "claude-opus-4-6",
|
||||||
group: "main",
|
group: "main",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Atlas",
|
key: "atlas",
|
||||||
display: "Atlas",
|
display: "Atlas",
|
||||||
descZh: "任务管理者",
|
descKey: "omo.agentDesc.atlas",
|
||||||
descEn: "Task manager",
|
tooltipKey: "omo.agentTooltip.atlas",
|
||||||
recommended: "kimi-k2.5",
|
recommended: "kimi-k2.5",
|
||||||
group: "main",
|
group: "main",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "oracle",
|
key: "oracle",
|
||||||
display: "Oracle",
|
display: "Oracle",
|
||||||
descZh: "战略顾问",
|
descKey: "omo.agentDesc.oracle",
|
||||||
descEn: "Strategic advisor",
|
tooltipKey: "omo.agentTooltip.oracle",
|
||||||
recommended: "gpt-5.3",
|
recommended: "gpt-5.3",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "librarian",
|
key: "librarian",
|
||||||
display: "Librarian",
|
display: "Librarian",
|
||||||
descZh: "多仓库研究员",
|
descKey: "omo.agentDesc.librarian",
|
||||||
descEn: "Multi-repo researcher",
|
tooltipKey: "omo.agentTooltip.librarian",
|
||||||
recommended: "glm-4.7",
|
recommended: "glm-4.7",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "explore",
|
key: "explore",
|
||||||
display: "Explore",
|
display: "Explore",
|
||||||
descZh: "快速代码搜索",
|
descKey: "omo.agentDesc.explore",
|
||||||
descEn: "Fast code search",
|
tooltipKey: "omo.agentTooltip.explore",
|
||||||
recommended: "grok-code-fast-1",
|
recommended: "grok-code-fast-1",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "multimodal-looker",
|
key: "multimodal-looker",
|
||||||
display: "Multimodal-Looker",
|
display: "Multimodal-Looker",
|
||||||
descZh: "媒体分析器",
|
descKey: "omo.agentDesc.multimodalLooker",
|
||||||
descEn: "Media analyzer",
|
tooltipKey: "omo.agentTooltip.multimodalLooker",
|
||||||
recommended: "gemini-3-flash",
|
recommended: "gemini-3-flash",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Metis",
|
key: "metis",
|
||||||
display: "Metis",
|
display: "Metis",
|
||||||
descZh: "规划前分析顾问",
|
descKey: "omo.agentDesc.metis",
|
||||||
descEn: "Pre-plan analysis advisor",
|
tooltipKey: "omo.agentTooltip.metis",
|
||||||
recommended: "claude-opus-4-6",
|
recommended: "claude-opus-4-6",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Momus",
|
key: "momus",
|
||||||
display: "Momus",
|
display: "Momus",
|
||||||
descZh: "计划审查者",
|
descKey: "omo.agentDesc.momus",
|
||||||
descEn: "Plan reviewer",
|
tooltipKey: "omo.agentTooltip.momus",
|
||||||
recommended: "gpt-5.3",
|
recommended: "gpt-5.3",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Sisyphus-Junior",
|
key: "sisyphus-junior",
|
||||||
display: "Sisyphus-Junior",
|
display: "Sisyphus-Junior",
|
||||||
descZh: "委托任务执行器",
|
descKey: "omo.agentDesc.sisyphusJunior",
|
||||||
descEn: "Delegated task executor",
|
tooltipKey: "omo.agentTooltip.sisyphusJunior",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -135,57 +135,57 @@ export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [
|
|||||||
{
|
{
|
||||||
key: "visual-engineering",
|
key: "visual-engineering",
|
||||||
display: "Visual Engineering",
|
display: "Visual Engineering",
|
||||||
descZh: "视觉/前端工程",
|
descKey: "omo.categoryDesc.visualEngineering",
|
||||||
descEn: "Visual/frontend engineering",
|
tooltipKey: "omo.categoryTooltip.visualEngineering",
|
||||||
recommended: "gemini-3-pro",
|
recommended: "gemini-3-pro",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "ultrabrain",
|
key: "ultrabrain",
|
||||||
display: "Ultrabrain",
|
display: "Ultrabrain",
|
||||||
descZh: "超级思考",
|
descKey: "omo.categoryDesc.ultrabrain",
|
||||||
descEn: "Ultra thinking",
|
tooltipKey: "omo.categoryTooltip.ultrabrain",
|
||||||
recommended: "claude-opus-4-6",
|
recommended: "claude-opus-4-6",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "deep",
|
key: "deep",
|
||||||
display: "Deep",
|
display: "Deep",
|
||||||
descZh: "深度工作",
|
descKey: "omo.categoryDesc.deep",
|
||||||
descEn: "Deep work",
|
tooltipKey: "omo.categoryTooltip.deep",
|
||||||
recommended: "gpt-5.3-codex",
|
recommended: "gpt-5.3-codex",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "artistry",
|
key: "artistry",
|
||||||
display: "Artistry",
|
display: "Artistry",
|
||||||
descZh: "创意/文艺",
|
descKey: "omo.categoryDesc.artistry",
|
||||||
descEn: "Creative/artistic",
|
tooltipKey: "omo.categoryTooltip.artistry",
|
||||||
recommended: "claude-opus-4-6",
|
recommended: "claude-opus-4-6",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "quick",
|
key: "quick",
|
||||||
display: "Quick",
|
display: "Quick",
|
||||||
descZh: "快速响应",
|
descKey: "omo.categoryDesc.quick",
|
||||||
descEn: "Quick response",
|
tooltipKey: "omo.categoryTooltip.quick",
|
||||||
recommended: "gemini-3-flash",
|
recommended: "gemini-3-flash",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "unspecified-low",
|
key: "unspecified-low",
|
||||||
display: "Unspecified Low",
|
display: "Unspecified Low",
|
||||||
descZh: "通用低配",
|
descKey: "omo.categoryDesc.unspecifiedLow",
|
||||||
descEn: "General low tier",
|
tooltipKey: "omo.categoryTooltip.unspecifiedLow",
|
||||||
recommended: "gemini-3-flash",
|
recommended: "gemini-3-flash",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "unspecified-high",
|
key: "unspecified-high",
|
||||||
display: "Unspecified High",
|
display: "Unspecified High",
|
||||||
descZh: "通用高配",
|
descKey: "omo.categoryDesc.unspecifiedHigh",
|
||||||
descEn: "General high tier",
|
tooltipKey: "omo.categoryTooltip.unspecifiedHigh",
|
||||||
recommended: "gpt-5.3-codex",
|
recommended: "gpt-5.3-codex",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "writing",
|
key: "writing",
|
||||||
display: "Writing",
|
display: "Writing",
|
||||||
descZh: "写作",
|
descKey: "omo.categoryDesc.writing",
|
||||||
descEn: "Writing",
|
tooltipKey: "omo.categoryTooltip.writing",
|
||||||
recommended: "claude-opus-4-6",
|
recommended: "claude-opus-4-6",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -316,6 +316,17 @@ export const OMO_CLAUDE_CODE_PLACEHOLDER = `{
|
|||||||
"plugins": true
|
"plugins": true
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
export function parseOmoOtherFieldsObject(
|
||||||
|
raw: string,
|
||||||
|
): Record<string, unknown> | undefined {
|
||||||
|
if (!raw.trim()) return undefined;
|
||||||
|
const parsed: unknown = JSON.parse(raw);
|
||||||
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return parsed as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
export function mergeOmoConfigPreview(
|
export function mergeOmoConfigPreview(
|
||||||
global: OmoGlobalConfig,
|
global: OmoGlobalConfig,
|
||||||
agents: Record<string, Record<string, unknown>>,
|
agents: Record<string, Record<string, unknown>>,
|
||||||
@@ -351,7 +362,8 @@ export function mergeOmoConfigPreview(
|
|||||||
if (Object.keys(agents).length > 0) result["agents"] = agents;
|
if (Object.keys(agents).length > 0) result["agents"] = agents;
|
||||||
if (Object.keys(categories).length > 0) result["categories"] = categories;
|
if (Object.keys(categories).length > 0) result["categories"] = categories;
|
||||||
try {
|
try {
|
||||||
const other = JSON.parse(otherFieldsStr || "{}");
|
const other = parseOmoOtherFieldsObject(otherFieldsStr);
|
||||||
|
if (!other) return result;
|
||||||
for (const [k, v] of Object.entries(other)) {
|
for (const [k, v] of Object.entries(other)) {
|
||||||
result[k] = v;
|
result[k] = v;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
mergeCustomModelsIntoStore,
|
||||||
|
type CustomModelItem,
|
||||||
|
} from "@/components/providers/forms/OmoFormFields";
|
||||||
|
|
||||||
|
describe("mergeCustomModelsIntoStore", () => {
|
||||||
|
it("保留自定义项高级字段,并在模型变更时仅按需清理非法 variant", () => {
|
||||||
|
const store = {
|
||||||
|
sisyphus: { model: "builtin-model" },
|
||||||
|
"custom-agent": {
|
||||||
|
model: "model-a",
|
||||||
|
variant: "fast",
|
||||||
|
temperature: 0.2,
|
||||||
|
permission: { edit: "allow" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const customs: CustomModelItem[] = [
|
||||||
|
{ key: "custom-agent", model: "model-b", sourceKey: "custom-agent" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const merged = mergeCustomModelsIntoStore(
|
||||||
|
store,
|
||||||
|
new Set(["sisyphus"]),
|
||||||
|
customs,
|
||||||
|
{ "model-b": ["precise"] },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(merged.sisyphus).toEqual({ model: "builtin-model" });
|
||||||
|
expect(merged["custom-agent"]).toEqual({
|
||||||
|
model: "model-b",
|
||||||
|
temperature: 0.2,
|
||||||
|
permission: { edit: "allow" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("重命名自定义 key 时迁移原有 variant 和高级字段", () => {
|
||||||
|
const store = {
|
||||||
|
sisyphus: { model: "builtin-model" },
|
||||||
|
"custom-agent-old": {
|
||||||
|
model: "model-a",
|
||||||
|
variant: "fast",
|
||||||
|
maxTokens: 8192,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const customs: CustomModelItem[] = [
|
||||||
|
{
|
||||||
|
key: "custom-agent-new",
|
||||||
|
sourceKey: "custom-agent-old",
|
||||||
|
model: "model-a",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const merged = mergeCustomModelsIntoStore(
|
||||||
|
store,
|
||||||
|
new Set(["sisyphus"]),
|
||||||
|
customs,
|
||||||
|
{ "model-a": ["fast", "balanced"] },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(merged["custom-agent-old"]).toBeUndefined();
|
||||||
|
expect(merged["custom-agent-new"]).toEqual({
|
||||||
|
model: "model-a",
|
||||||
|
variant: "fast",
|
||||||
|
maxTokens: 8192,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("custom 列表为空时移除旧自定义项但保留内置项", () => {
|
||||||
|
const store = {
|
||||||
|
sisyphus: { model: "builtin-model" },
|
||||||
|
hephaestus: { model: "builtin-model-2" },
|
||||||
|
"custom-agent": { model: "model-a", temperature: 0.3 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const merged = mergeCustomModelsIntoStore(
|
||||||
|
store,
|
||||||
|
new Set(["sisyphus", "hephaestus"]),
|
||||||
|
[],
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(merged).toEqual({
|
||||||
|
sisyphus: { model: "builtin-model" },
|
||||||
|
hephaestus: { model: "builtin-model-2" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("清空 model 时保留高级字段并移除 model/variant", () => {
|
||||||
|
const store = {
|
||||||
|
sisyphus: { model: "builtin-model" },
|
||||||
|
"custom-agent": {
|
||||||
|
model: "model-a",
|
||||||
|
variant: "fast",
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const customs: CustomModelItem[] = [
|
||||||
|
{ key: "custom-agent", model: "", sourceKey: "custom-agent" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const merged = mergeCustomModelsIntoStore(
|
||||||
|
store,
|
||||||
|
new Set(["sisyphus"]),
|
||||||
|
customs,
|
||||||
|
{ "model-a": ["fast"] },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(merged["custom-agent"]).toEqual({ temperature: 0.7 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
mergeOmoConfigPreview,
|
||||||
|
parseOmoOtherFieldsObject,
|
||||||
|
type OmoGlobalConfig,
|
||||||
|
} from "@/types/omo";
|
||||||
|
|
||||||
|
const EMPTY_GLOBAL: OmoGlobalConfig = {
|
||||||
|
id: "global",
|
||||||
|
disabledAgents: [],
|
||||||
|
disabledMcps: [],
|
||||||
|
disabledHooks: [],
|
||||||
|
disabledSkills: [],
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("parseOmoOtherFieldsObject", () => {
|
||||||
|
it("解析对象 JSON", () => {
|
||||||
|
expect(parseOmoOtherFieldsObject('{ "foo": 1 }')).toEqual({ foo: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("数组/字符串返回 undefined", () => {
|
||||||
|
expect(parseOmoOtherFieldsObject('["a"]')).toBeUndefined();
|
||||||
|
expect(parseOmoOtherFieldsObject('"hello"')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("非法 JSON 抛出异常", () => {
|
||||||
|
expect(() => parseOmoOtherFieldsObject("{")).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mergeOmoConfigPreview", () => {
|
||||||
|
it("只合并 otherFields 的对象值,忽略数组", () => {
|
||||||
|
const mergedFromArray = mergeOmoConfigPreview(
|
||||||
|
EMPTY_GLOBAL,
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
'["a", "b"]',
|
||||||
|
);
|
||||||
|
expect(mergedFromArray).toEqual({});
|
||||||
|
|
||||||
|
const mergedFromObject = mergeOmoConfigPreview(
|
||||||
|
EMPTY_GLOBAL,
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
'{ "foo": "bar" }',
|
||||||
|
);
|
||||||
|
expect(mergedFromObject).toEqual({ foo: "bar" });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user