Compare commits

..

1 Commits

Author SHA1 Message Date
YoVinchen 38e4ed8e9a fix(opencode): reject save when no models configured
Add validation to require at least one model before saving an OpenCode
provider. Shows a localized toast error when models are empty.
2026-02-05 18:29:05 +08:00
28 changed files with 136 additions and 136 deletions
+1 -1
View File
@@ -755,7 +755,7 @@ fn launch_macos_open_app(
let output = cmd
.output()
.map_err(|e| format!("启动 {app_name} 失败: {e}"))?;
.map_err(|e| format!("启动 {} 失败: {e}", app_name))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
+2 -2
View File
@@ -12,8 +12,8 @@ mod plugin;
mod prompt;
mod provider;
mod proxy;
mod session_manager;
mod settings;
mod session_manager;
pub mod skill;
mod stream_check;
mod usage;
@@ -30,8 +30,8 @@ pub use plugin::*;
pub use prompt::*;
pub use provider::*;
pub use proxy::*;
pub use session_manager::*;
pub use settings::*;
pub use session_manager::*;
pub use skill::*;
pub use stream_check::*;
pub use usage::*;
+1 -1
View File
@@ -20,8 +20,8 @@ mod prompt_files;
mod provider;
mod provider_defaults;
mod proxy;
mod services;
mod session_manager;
mod services;
mod settings;
mod store;
mod tray;
@@ -55,12 +55,17 @@ pub fn load_messages(path: &Path) -> Result<Vec<SessionMessage>, String> {
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string();
let content = message.get("content").map(extract_text).unwrap_or_default();
let content = message
.get("content")
.map(extract_text)
.unwrap_or_default();
if content.trim().is_empty() {
continue;
}
let ts = value.get("timestamp").and_then(parse_timestamp_to_ms);
let ts = value
.get("timestamp")
.and_then(parse_timestamp_to_ms);
messages.push(SessionMessage { role, content, ts });
}
@@ -122,7 +127,10 @@ fn parse_session(path: &Path) -> Option<SessionMeta> {
None => continue,
};
let text = message.get("content").map(extract_text).unwrap_or_default();
let text = message
.get("content")
.map(extract_text)
.unwrap_or_default();
if text.trim().is_empty() {
continue;
}
@@ -2,8 +2,8 @@ use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use regex::Regex;
use serde_json::Value;
use regex::Regex;
use crate::codex_config::get_codex_config_dir;
use crate::session_manager::{SessionMessage, SessionMeta};
@@ -60,12 +60,17 @@ pub fn load_messages(path: &Path) -> Result<Vec<SessionMessage>, String> {
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string();
let content = payload.get("content").map(extract_text).unwrap_or_default();
let content = payload
.get("content")
.map(extract_text)
.unwrap_or_default();
if content.trim().is_empty() {
continue;
}
let ts = value.get("timestamp").and_then(parse_timestamp_to_ms);
let ts = value
.get("timestamp")
.and_then(parse_timestamp_to_ms);
messages.push(SessionMessage { role, content, ts });
}
@@ -134,7 +139,10 @@ fn parse_session(path: &Path) -> Option<SessionMeta> {
continue;
}
let text = payload.get("content").map(extract_text).unwrap_or_default();
let text = payload
.get("content")
.map(extract_text)
.unwrap_or_default();
if text.trim().is_empty() {
continue;
}
@@ -166,9 +174,10 @@ fn parse_session(path: &Path) -> Option<SessionMeta> {
fn infer_session_id_from_filename(path: &Path) -> Option<String> {
let file_name = path.file_name()?.to_string_lossy();
let re =
Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
.ok()?;
let re = Regex::new(
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
)
.ok()?;
re.find(&file_name).map(|mat| mat.as_str().to_string())
}
@@ -13,7 +13,7 @@ pub fn extract_text(content: &Value) -> String {
Value::String(text) => text.to_string(),
Value::Array(items) => items
.iter()
.filter_map(extract_text_from_item)
.filter_map(|item| extract_text_from_item(item))
.filter(|text| !text.trim().is_empty())
.collect::<Vec<_>>()
.join("\n"),
@@ -68,10 +68,10 @@ pub fn path_basename(value: &str) -> Option<String> {
if trimmed.is_empty() {
return None;
}
let normalized = trimmed.trim_end_matches(['/', '\\']);
let normalized = trimmed.trim_end_matches(|c| c == '/' || c == '\\');
let last = normalized
.split(['/', '\\'])
.next_back()
.last()
.filter(|segment| !segment.is_empty())?;
Some(last.to_string())
}
@@ -32,8 +32,9 @@ fn launch_macos_terminal(command: &str, cwd: Option<&str>) -> Result<(), String>
let script = format!(
r#"tell application "Terminal"
activate
do script "{escaped}"
end tell"#
do script "{}"
end tell"#,
escaped
);
let status = Command::new("osascript")
@@ -58,9 +59,10 @@ fn launch_iterm(command: &str, cwd: Option<&str>) -> Result<(), String> {
activate
create window with default profile
tell current session of current window
write text "{escaped}"
write text "{}"
end tell
end tell"#
end tell"#,
escaped
);
let status = Command::new("osascript")
@@ -86,7 +88,7 @@ fn launch_ghostty(command: &str, cwd: Option<&str>) -> Result<(), String> {
// Note: The user's error output didn't show the working dir arg failure, so we assume flag is okay or we stick to compatible ones.
// Documentation says --working-directory is supported in CLI.
let work_dir_arg = if let Some(dir) = cwd {
format!("--working-directory={dir}")
format!("--working-directory={}", dir)
} else {
"".to_string()
};
@@ -249,7 +251,7 @@ fn build_shell_command(command: &str, cwd: Option<&str>) -> String {
fn shell_escape(value: &str) -> String {
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
format!("\"{}\"", escaped)
}
fn escape_osascript(value: &str) -> String {
+21 -23
View File
@@ -175,7 +175,7 @@ function App() {
// 当前应用代理实际使用的供应商 ID(从 active_targets 中获取)
const activeProviderId = useMemo(() => {
const target = proxyStatus?.active_targets?.find(
(t) => t.app_type === activeApp,
(t) => t.app_type === activeApp
);
return target?.provider_id;
}, [proxyStatus?.active_targets, activeApp]);
@@ -208,7 +208,7 @@ function App() {
if (event.appType === activeApp) {
await refetch();
}
},
}
);
} catch (error) {
console.error("[App] Failed to subscribe provider switch event", error);
@@ -242,7 +242,7 @@ function App() {
} catch (error) {
console.error(
"[App] Failed to subscribe universal-provider-synced event",
error,
error
);
}
};
@@ -270,7 +270,7 @@ function App() {
} catch (error) {
console.error(
"[App] Failed to check environment conflicts on startup:",
error,
error
);
}
};
@@ -286,7 +286,7 @@ function App() {
if (migrated) {
toast.success(
t("migration.success", { defaultValue: "配置迁移成功" }),
{ closeButton: true },
{ closeButton: true }
);
}
} catch (error) {
@@ -302,7 +302,7 @@ function App() {
const checkSkillsMigration = async () => {
try {
const result = await invoke<{ count: number; error?: string } | null>(
"get_skills_migration_result",
"get_skills_migration_result"
);
if (result?.error) {
toast.error(t("migration.skillsFailed"), {
@@ -336,10 +336,10 @@ function App() {
// 合并新检测到的冲突
setEnvConflicts((prev) => {
const existingKeys = new Set(
prev.map((c) => `${c.varName}:${c.sourcePath}`),
prev.map((c) => `${c.varName}:${c.sourcePath}`)
);
const newConflicts = conflicts.filter(
(c) => !existingKeys.has(`${c.varName}:${c.sourcePath}`),
(c) => !existingKeys.has(`${c.varName}:${c.sourcePath}`)
);
return [...prev, ...newConflicts];
});
@@ -351,7 +351,7 @@ function App() {
} catch (error) {
console.error(
"[App] Failed to check environment conflicts on app switch:",
error,
error
);
}
};
@@ -433,7 +433,7 @@ function App() {
t("notifications.removeFromConfigSuccess", {
defaultValue: "已从配置移除",
}),
{ closeButton: true },
{ closeButton: true }
);
} else {
// Delete from database
@@ -445,7 +445,7 @@ function App() {
// Generate a unique provider key for OpenCode duplication
const generateUniqueOpencodeKey = (
originalKey: string,
existingKeys: string[],
existingKeys: string[]
): string => {
const baseKey = `${originalKey}-copy`;
@@ -487,7 +487,7 @@ function App() {
const existingKeys = Object.keys(providers);
duplicatedProvider.providerKey = generateUniqueOpencodeKey(
provider.id,
existingKeys,
existingKeys
);
}
@@ -498,7 +498,7 @@ function App() {
(p) =>
p.sortIndex !== undefined &&
p.sortIndex >= newSortIndex! &&
p.id !== provider.id,
p.id !== provider.id
)
.map((p) => ({
id: p.id,
@@ -514,7 +514,7 @@ function App() {
toast.error(
t("provider.sortUpdateFailed", {
defaultValue: "排序更新失败",
}),
})
);
return; // 如果排序更新失败,不继续添加
}
@@ -532,7 +532,7 @@ function App() {
toast.success(
t("provider.terminalOpened", {
defaultValue: "终端已打开",
}),
})
);
} catch (error) {
console.error("[App] Failed to open terminal", error);
@@ -540,7 +540,7 @@ function App() {
toast.error(
t("provider.terminalOpenFailed", {
defaultValue: "打开终端失败",
}) + (errorMessage ? `: ${errorMessage}` : ""),
}) + (errorMessage ? `: ${errorMessage}` : "")
);
}
};
@@ -721,7 +721,7 @@ function App() {
} catch (error) {
console.error(
"[App] Failed to re-check conflicts after deletion:",
error,
error
);
}
}}
@@ -755,9 +755,7 @@ function App() {
size="icon"
onClick={() =>
setCurrentView(
currentView === "skillsDiscovery"
? "skills"
: "providers",
currentView === "skillsDiscovery" ? "skills" : "providers"
)
}
className="mr-2 rounded-lg"
@@ -790,7 +788,7 @@ function App() {
"text-xl font-semibold transition-colors",
isProxyRunning && isCurrentAppTakeoverActive
? "text-emerald-500 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-300"
: "text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300",
: "text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
)}
>
CC Switch
@@ -936,7 +934,7 @@ function App() {
"transition-all duration-300 ease-in-out overflow-hidden",
isCurrentAppTakeoverActive
? "opacity-100 max-w-[100px] scale-100"
: "opacity-0 max-w-0 scale-75 pointer-events-none",
: "opacity-0 max-w-0 scale-75 pointer-events-none"
)}
>
<FailoverToggle activeApp={activeApp} />
@@ -964,7 +962,7 @@ function App() {
"transition-all duration-200 ease-in-out overflow-hidden",
hasSkillsSupport
? "opacity-100 w-8 scale-100 px-2"
: "opacity-0 w-0 scale-75 pointer-events-none px-0 -ml-1",
: "opacity-0 w-0 scale-75 pointer-events-none px-0 -ml-1"
)}
title={t("skills.manage")}
>
+1 -4
View File
@@ -8,10 +8,7 @@ interface AppCountBarProps {
counts: Record<AppId, number>;
}
export const AppCountBar: React.FC<AppCountBarProps> = ({
totalLabel,
counts,
}) => {
export const AppCountBar: React.FC<AppCountBarProps> = ({ totalLabel, counts }) => {
return (
<div className="flex-shrink-0 py-4 glass rounded-xl border border-white/10 mb-4 px-6 flex items-center justify-between gap-4">
<Badge variant="outline" className="bg-background/50 h-7 px-3">
+5 -9
View File
@@ -12,10 +12,7 @@ interface AppToggleGroupProps {
onToggle: (app: AppId, enabled: boolean) => void;
}
export const AppToggleGroup: React.FC<AppToggleGroupProps> = ({
apps,
onToggle,
}) => {
export const AppToggleGroup: React.FC<AppToggleGroupProps> = ({ apps, onToggle }) => {
return (
<div className="flex items-center gap-1.5 flex-shrink-0">
{APP_IDS.map((app) => {
@@ -28,17 +25,16 @@ export const AppToggleGroup: React.FC<AppToggleGroupProps> = ({
type="button"
onClick={() => onToggle(app, !enabled)}
className={`w-7 h-7 rounded-lg flex items-center justify-center transition-all ${
enabled ? activeClass : "opacity-35 hover:opacity-70"
enabled
? activeClass
: "opacity-35 hover:opacity-70"
}`}
>
{icon}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>
{label}
{enabled ? " ✓" : ""}
</p>
<p>{label}{enabled ? " ✓" : ""}</p>
</TooltipContent>
</Tooltip>
);
+1 -4
View File
@@ -5,10 +5,7 @@ interface ListItemRowProps {
children: React.ReactNode;
}
export const ListItemRow: React.FC<ListItemRowProps> = ({
isLast,
children,
}) => {
export const ListItemRow: React.FC<ListItemRowProps> = ({ isLast, children }) => {
return (
<div
className={`group flex items-center gap-3 px-4 py-2.5 hover:bg-muted/50 transition-colors ${
+2 -7
View File
@@ -245,9 +245,7 @@ const UnifiedMcpListItem: React.FC<UnifiedMcpListItemProps> = ({
<ListItemRow isLast={isLast}>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="font-medium text-sm text-foreground truncate">
{name}
</span>
<span className="font-medium text-sm text-foreground truncate">{name}</span>
{docsUrl && (
<button
type="button"
@@ -260,10 +258,7 @@ const UnifiedMcpListItem: React.FC<UnifiedMcpListItemProps> = ({
)}
</div>
{description && (
<p
className="text-xs text-muted-foreground truncate"
title={description}
>
<p className="text-xs text-muted-foreground truncate" title={description}>
{description}
</p>
)}
@@ -746,7 +746,7 @@ export function ProviderForm({
return;
}
// OpenCode: validate provider key
// OpenCode: validate provider key and models
if (appId === "opencode") {
const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
if (!opencodeProviderKey.trim()) {
@@ -761,6 +761,11 @@ export function ProviderForm({
toast.error(t("opencode.providerKeyDuplicate"));
return;
}
// Validate that at least one model is configured
if (Object.keys(opencodeModels).length === 0) {
toast.error(t("opencode.modelsRequired"));
return;
}
}
// 非官方供应商必填校验:端点和 API Key
+2 -2
View File
@@ -40,7 +40,7 @@ export function SessionItem({
"w-full text-left rounded-lg px-3 py-2.5 transition-all group",
isSelected
? "bg-primary/10 border border-primary/30"
: "hover:bg-muted/60 border border-transparent",
: "hover:bg-muted/60 border border-transparent"
)}
>
<div className="flex items-center gap-2 mb-1">
@@ -62,7 +62,7 @@ export function SessionItem({
<ChevronRight
className={cn(
"size-4 text-muted-foreground/50 shrink-0 transition-transform",
isSelected && "text-primary rotate-90",
isSelected && "text-primary rotate-90"
)}
/>
</div>
+13 -15
View File
@@ -56,7 +56,7 @@ export function SessionManagerPage() {
const messagesEndRef = useRef<HTMLDivElement | null>(null);
const messageRefs = useRef<Map<number, HTMLDivElement>>(new Map());
const [activeMessageIndex, setActiveMessageIndex] = useState<number | null>(
null,
null
);
const [tocDialogOpen, setTocDialogOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false);
@@ -83,7 +83,7 @@ export function SessionManagerPage() {
}
const exists = selectedKey
? filteredSessions.some(
(session) => getSessionKey(session) === selectedKey,
(session) => getSessionKey(session) === selectedKey
)
: false;
if (!exists) {
@@ -95,7 +95,7 @@ export function SessionManagerPage() {
if (!selectedKey) return null;
return (
filteredSessions.find(
(session) => getSessionKey(session) === selectedKey,
(session) => getSessionKey(session) === selectedKey
) || null
);
}, [filteredSessions, selectedKey]);
@@ -103,7 +103,7 @@ export function SessionManagerPage() {
const { data: messages = [], isLoading: isLoadingMessages } =
useSessionMessagesQuery(
selectedSession?.providerId,
selectedSession?.sourcePath,
selectedSession?.sourcePath
);
// 提取用户消息用于目录
@@ -147,7 +147,7 @@ export function SessionManagerPage() {
} catch (error) {
toast.error(
extractErrorMessage(error) ||
t("common.error", { defaultValue: "Copy failed" }),
t("common.error", { defaultValue: "Copy failed" })
);
}
};
@@ -158,7 +158,7 @@ export function SessionManagerPage() {
if (!isMac()) {
await handleCopy(
selectedSession.resumeCommand,
t("sessionManager.resumeCommandCopied"),
t("sessionManager.resumeCommandCopied")
);
return;
}
@@ -240,16 +240,14 @@ export function SessionManagerPage() {
setIsSearchOpen(true);
setTimeout(
() => searchInputRef.current?.focus(),
0,
0
);
}}
>
<Search className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
{t("sessionManager.searchSessions")}
</TooltipContent>
<TooltipContent>{t("sessionManager.searchSessions")}</TooltipContent>
</Tooltip>
<Select
@@ -389,7 +387,7 @@ export function SessionManagerPage() {
<span className="shrink-0">
<ProviderIcon
icon={getProviderIconName(
selectedSession.providerId,
selectedSession.providerId
)}
name={selectedSession.providerId}
size={20}
@@ -412,7 +410,7 @@ export function SessionManagerPage() {
<span>
{formatTimestamp(
selectedSession.lastActiveAt ??
selectedSession.createdAt,
selectedSession.createdAt
)}
</span>
</div>
@@ -424,7 +422,7 @@ export function SessionManagerPage() {
onClick={() =>
void handleCopy(
selectedSession.projectDir!,
t("sessionManager.projectDirCopied"),
t("sessionManager.projectDirCopied")
)
}
className="flex items-center gap-1 hover:text-foreground transition-colors"
@@ -499,7 +497,7 @@ export function SessionManagerPage() {
onClick={() =>
void handleCopy(
selectedSession.resumeCommand!,
t("sessionManager.resumeCommandCopied"),
t("sessionManager.resumeCommandCopied")
)
}
>
@@ -561,7 +559,7 @@ export function SessionManagerPage() {
content,
t("sessionManager.messageCopied", {
defaultValue: "已复制消息内容",
}),
})
)
}
/>
@@ -37,7 +37,7 @@ export function SessionMessageItem({
: message.role.toLowerCase() === "assistant"
? "bg-blue-500/5 border-blue-500/20 mr-8"
: "bg-muted/40 border-border/60",
isActive && "ring-2 ring-primary ring-offset-2",
isActive && "ring-2 ring-primary ring-offset-2"
)}
>
<Tooltip>
+2 -2
View File
@@ -48,7 +48,7 @@ export function SessionTocSidebar({
className={cn(
"w-full text-left px-2 py-1.5 rounded text-xs transition-colors",
"hover:bg-muted/80 text-muted-foreground hover:text-foreground",
"flex items-start gap-2",
"flex items-start gap-2"
)}
>
<span className="shrink-0 w-4 h-4 rounded-full bg-primary/10 text-primary text-[10px] flex items-center justify-center font-medium">
@@ -118,7 +118,7 @@ export function SessionTocDialog({
"w-full text-left px-3 py-2.5 rounded-lg text-sm transition-all",
"hover:bg-primary/10 text-foreground",
"flex items-start gap-3",
"focus:outline-none focus:ring-2 focus:ring-primary focus:ring-inset",
"focus:outline-none focus:ring-2 focus:ring-primary focus:ring-inset"
)}
>
<span className="shrink-0 w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs flex items-center justify-center font-semibold">
+6 -3
View File
@@ -19,7 +19,7 @@ export const formatTimestamp = (value?: number) => {
export const formatRelativeTime = (
value: number | undefined,
t: (key: string, options?: Record<string, unknown>) => string,
t: (key: string, options?: Record<string, unknown>) => string
) => {
if (!value) return "";
const now = Date.now();
@@ -37,7 +37,7 @@ export const formatRelativeTime = (
export const getProviderLabel = (
providerId: string,
t: (key: string) => string,
t: (key: string) => string
) => {
const key = `apps.${providerId}`;
const translated = t(key);
@@ -60,7 +60,10 @@ export const getRoleTone = (role: string) => {
return "text-muted-foreground";
};
export const getRoleLabel = (role: string, t: (key: string) => string) => {
export const getRoleLabel = (
role: string,
t: (key: string) => string
) => {
const normalized = role.toLowerCase();
if (normalized === "assistant") return "AI";
if (normalized === "user") return t("sessionManager.roleUser");
+8 -11
View File
@@ -63,7 +63,11 @@ const UnifiedSkillsPanel = React.forwardRef<
return counts;
}, [skills]);
const handleToggleApp = async (id: string, app: AppId, enabled: boolean) => {
const handleToggleApp = async (
id: string,
app: AppId,
enabled: boolean,
) => {
try {
await toggleAppMutation.mutateAsync({ id, app, enabled });
} catch (error) {
@@ -253,9 +257,7 @@ const InstalledSkillListItem: React.FC<InstalledSkillListItemProps> = ({
<ListItemRow isLast={isLast}>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="font-medium text-sm text-foreground truncate">
{skill.name}
</span>
<span className="font-medium text-sm text-foreground truncate">{skill.name}</span>
{skill.readmeUrl && (
<button
type="button"
@@ -265,15 +267,10 @@ const InstalledSkillListItem: React.FC<InstalledSkillListItemProps> = ({
<ExternalLink size={12} />
</button>
)}
<span className="text-xs text-muted-foreground/50 flex-shrink-0">
{sourceLabel}
</span>
<span className="text-xs text-muted-foreground/50 flex-shrink-0">{sourceLabel}</span>
</div>
{skill.description && (
<p
className="text-xs text-muted-foreground truncate"
title={skill.description}
>
<p className="text-xs text-muted-foreground truncate" title={skill.description}>
{skill.description}
</p>
)}
+1 -1
View File
@@ -36,7 +36,7 @@ const ScrollBar = React.forwardRef<
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
className
)}
{...props}
>
+1 -1
View File
@@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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,
className
)}
{...props}
/>
+9 -24
View File
@@ -16,40 +16,25 @@ export const APP_ICON_MAP: Record<AppId, AppConfig> = {
claude: {
label: "Claude",
icon: <ClaudeIcon size={14} />,
activeClass:
"bg-orange-500/10 ring-1 ring-orange-500/20 hover:bg-orange-500/20 text-orange-600 dark:text-orange-400",
badgeClass:
"bg-orange-500/10 text-orange-700 dark:text-orange-300 hover:bg-orange-500/20 border-0 gap-1.5",
activeClass: "bg-orange-500/10 ring-1 ring-orange-500/20 hover:bg-orange-500/20 text-orange-600 dark:text-orange-400",
badgeClass: "bg-orange-500/10 text-orange-700 dark:text-orange-300 hover:bg-orange-500/20 border-0 gap-1.5",
},
codex: {
label: "Codex",
icon: <CodexIcon size={14} />,
activeClass:
"bg-green-500/10 ring-1 ring-green-500/20 hover:bg-green-500/20 text-green-600 dark:text-green-400",
badgeClass:
"bg-green-500/10 text-green-700 dark:text-green-300 hover:bg-green-500/20 border-0 gap-1.5",
activeClass: "bg-green-500/10 ring-1 ring-green-500/20 hover:bg-green-500/20 text-green-600 dark:text-green-400",
badgeClass: "bg-green-500/10 text-green-700 dark:text-green-300 hover:bg-green-500/20 border-0 gap-1.5",
},
gemini: {
label: "Gemini",
icon: <GeminiIcon size={14} />,
activeClass:
"bg-blue-500/10 ring-1 ring-blue-500/20 hover:bg-blue-500/20 text-blue-600 dark:text-blue-400",
badgeClass:
"bg-blue-500/10 text-blue-700 dark:text-blue-300 hover:bg-blue-500/20 border-0 gap-1.5",
activeClass: "bg-blue-500/10 ring-1 ring-blue-500/20 hover:bg-blue-500/20 text-blue-600 dark:text-blue-400",
badgeClass: "bg-blue-500/10 text-blue-700 dark:text-blue-300 hover:bg-blue-500/20 border-0 gap-1.5",
},
opencode: {
label: "OpenCode",
icon: (
<ProviderIcon
icon="opencode"
name="OpenCode"
size={14}
showFallback={false}
/>
),
activeClass:
"bg-indigo-500/10 ring-1 ring-indigo-500/20 hover:bg-indigo-500/20 text-indigo-600 dark:text-indigo-400",
badgeClass:
"bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-500/20 border-0 gap-1.5",
icon: <ProviderIcon icon="opencode" name="OpenCode" size={14} showFallback={false} />,
activeClass: "bg-indigo-500/10 ring-1 ring-indigo-500/20 hover:bg-indigo-500/20 text-indigo-600 dark:text-indigo-400",
badgeClass: "bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-500/20 border-0 gap-1.5",
},
};
+2 -2
View File
@@ -117,7 +117,7 @@ export function useSessionSearch({
.filter(
(session) =>
session &&
(providerFilter === "all" || session.providerId === providerFilter),
(providerFilter === "all" || session.providerId === providerFilter)
);
// 按时间排序
@@ -127,7 +127,7 @@ export function useSessionSearch({
return bTs - aTs;
});
},
[sessions, providerFilter],
[sessions, providerFilter]
);
return useMemo(() => ({ search, isIndexing }), [search, isIndexing]);
+1
View File
@@ -633,6 +633,7 @@
"modelId": "Model ID",
"modelName": "Display Name",
"noModels": "No models configured",
"modelsRequired": "Please add at least one model",
"providerKey": "Provider Key",
"providerKeyPlaceholder": "my-provider",
"providerKeyHint": "Unique identifier in config file. Cannot be changed after creation. Use lowercase letters, numbers, and hyphens only.",
+1
View File
@@ -633,6 +633,7 @@
"modelId": "モデル ID",
"modelName": "表示名",
"noModels": "モデルが設定されていません",
"modelsRequired": "モデルを少なくとも1つ追加してください",
"providerKey": "プロバイダーキー",
"providerKeyPlaceholder": "my-provider",
"providerKeyHint": "設定ファイルの一意の識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用できます。",
+1
View File
@@ -633,6 +633,7 @@
"modelId": "模型 ID",
"modelName": "显示名称",
"noModels": "暂无模型配置",
"modelsRequired": "请至少添加一个模型配置",
"providerKey": "供应商标识",
"providerKeyPlaceholder": "my-provider",
"providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符",
+1 -1
View File
@@ -8,7 +8,7 @@ export const sessionsApi = {
async getMessages(
providerId: string,
sourcePath: string,
sourcePath: string
): Promise<SessionMessage[]> {
return await invoke("get_session_messages", { providerId, sourcePath });
},
+9 -2
View File
@@ -91,7 +91,11 @@ export const skillsApi = {
},
/** 切换 Skill 的应用启用状态 */
async toggleApp(id: string, app: AppId, enabled: boolean): Promise<boolean> {
async toggleApp(
id: string,
app: AppId,
enabled: boolean,
): Promise<boolean> {
return await invoke("toggle_skill_app", { id, app, enabled });
},
@@ -129,7 +133,10 @@ export const skillsApi = {
},
/** 卸载技能(兼容旧 API) */
async uninstall(directory: string, app: AppId = "claude"): Promise<boolean> {
async uninstall(
directory: string,
app: AppId = "claude",
): Promise<boolean> {
if (app === "claude") {
return await invoke("uninstall_skill", { directory });
}