feat: improve app visibility settings and keyboard shortcuts

- Add ESC key navigation: return to previous view from full-screen panels
- Sync tray menu with app visibility settings (hide disabled apps)
- Add fnm (fast node manager) path support for CLI version scanning
- Fix useModelState being overwritten when user is editing model fields
- Fix EditProviderDialog showing stale data after save and reopen
- Refactor AppSwitcher to use loop instead of repetitive buttons
- Extract ToggleRow as reusable UI component
- Add domUtils for checking text-editable elements
This commit is contained in:
YoVinchen
2026-01-20 21:53:47 +08:00
parent 2b3c80703c
commit 8f1ad6e057
16 changed files with 415 additions and 142 deletions
+13
View File
@@ -322,6 +322,19 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
search_paths.push(std::path::PathBuf::from("C:\\Program Files\\nodejs"));
}
// 添加 fnm 路径支持
let fnm_base = home.join(".local/state/fnm_multishells");
if fnm_base.exists() {
if let Ok(entries) = std::fs::read_dir(&fnm_base) {
for entry in entries.flatten() {
let bin_path = entry.path().join("bin");
if bin_path.exists() {
search_paths.push(bin_path);
}
}
}
}
// 扫描 nvm 目录下的所有 node 版本
let nvm_base = home.join(".nvm/versions/node");
if nvm_base.exists() {
+46 -4
View File
@@ -16,6 +16,47 @@ pub struct CustomEndpoint {
pub last_used: Option<i64>,
}
fn default_true() -> bool {
true
}
/// 主页面显示的应用配置
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisibleApps {
#[serde(default = "default_true")]
pub claude: bool,
#[serde(default = "default_true")]
pub codex: bool,
#[serde(default = "default_true")]
pub gemini: bool,
#[serde(default = "default_true")]
pub opencode: bool,
}
impl Default for VisibleApps {
fn default() -> Self {
Self {
claude: true,
codex: true,
gemini: true,
opencode: true,
}
}
}
impl VisibleApps {
/// Check if the specified app is visible
pub fn is_visible(&self, app: &AppType) -> bool {
match app {
AppType::Claude => self.claude,
AppType::Codex => self.codex,
AppType::Gemini => self.gemini,
AppType::OpenCode => self.opencode,
}
}
}
/// 应用设置结构
///
/// 存储设备级别设置,保存在本地 `~/.cc-switch/settings.json`,不随数据库同步。
@@ -40,6 +81,10 @@ pub struct AppSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
// ===== 主页面显示的应用 =====
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visible_apps: Option<VisibleApps>,
// ===== 设备级目录覆盖 =====
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claude_config_dir: Option<String>,
@@ -73,10 +118,6 @@ fn default_minimize_to_tray_on_close() -> bool {
true
}
fn default_true() -> bool {
true
}
impl Default for AppSettings {
fn default() -> Self {
Self {
@@ -86,6 +127,7 @@ impl Default for AppSettings {
skip_claude_onboarding: true,
launch_on_startup: false,
language: None,
visible_apps: None,
claude_config_dir: None,
codex_config_dir: None,
gemini_config_dir: None,
+9
View File
@@ -318,6 +318,9 @@ pub fn create_tray_menu(
let app_settings = crate::settings::get_settings();
let tray_texts = TrayTexts::from_language(app_settings.language.as_deref().unwrap_or("zh"));
// Get visible apps setting, default to all visible
let visible_apps = app_settings.visible_apps.unwrap_or_default();
let mut menu_builder = MenuBuilder::new(app);
// 顶部:打开主界面
@@ -327,7 +330,13 @@ pub fn create_tray_menu(
menu_builder = menu_builder.item(&show_main_item).separator();
// 直接添加所有供应商到主菜单(扁平化结构,更简单可靠)
// Only add visible app sections
for section in TRAY_SECTIONS.iter() {
// Skip hidden apps
if !visible_apps.is_visible(&section.app_type) {
continue;
}
let app_type_str = section.app_type.as_str();
let providers = app_state.db.get_all_providers(app_type_str)?;
+56 -8
View File
@@ -17,9 +17,9 @@ import {
Download,
BarChart2,
} from "lucide-react";
import type { Provider } from "@/types";
import type { Provider, VisibleApps } from "@/types";
import type { EnvConflict } from "@/types/env";
import { useProvidersQuery } from "@/lib/query";
import { useProvidersQuery, useSettingsQuery } from "@/lib/query";
import {
providersApi,
settingsApi,
@@ -31,6 +31,7 @@ import { useProviderActions } from "@/hooks/useProviderActions";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import { useLastValidValue } from "@/hooks/useLastValidValue";
import { extractErrorMessage } from "@/utils/errorUtils";
import { isTextEditableTarget } from "@/utils/domUtils";
import { cn } from "@/lib/utils";
import { isWindows, isLinux } from "@/lib/platform";
import { AppSwitcher } from "@/components/AppSwitcher";
@@ -77,6 +78,31 @@ function App() {
const [settingsDefaultTab, setSettingsDefaultTab] = useState("general");
const [isAddOpen, setIsAddOpen] = useState(false);
// Get settings for visibleApps
const { data: settingsData } = useSettingsQuery();
const visibleApps: VisibleApps = settingsData?.visibleApps ?? {
claude: true,
codex: true,
gemini: true,
opencode: true,
};
// Get first visible app for fallback
const getFirstVisibleApp = (): AppId => {
if (visibleApps.claude) return "claude";
if (visibleApps.codex) return "codex";
if (visibleApps.gemini) return "gemini";
if (visibleApps.opencode) return "opencode";
return "claude"; // fallback
};
// If current active app is hidden, switch to first visible app
useEffect(() => {
if (!visibleApps[activeApp]) {
setActiveApp(getFirstVisibleApp());
}
}, [visibleApps, activeApp]);
const [editingProvider, setEditingProvider] = useState<Provider | null>(null);
const [usageProvider, setUsageProvider] = useState<Provider | null>(null);
// Confirm action state: 'remove' = remove from live config, 'delete' = delete from database
@@ -293,18 +319,40 @@ function App() {
checkEnvOnSwitch();
}, [activeApp]);
// 全局键盘快捷键
const currentViewRef = useRef(currentView);
useEffect(() => {
const handleGlobalShortcut = (event: KeyboardEvent) => {
if (event.key !== "," || !(event.metaKey || event.ctrlKey)) {
currentViewRef.current = currentView;
}, [currentView]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Cmd/Ctrl + , 打开设置
if (event.key === "," && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setCurrentView("settings");
return;
}
// ESC 键返回
if (event.key !== "Escape" || event.defaultPrevented) return;
// 如果有模态框打开(通过 overflow hidden 判断),则不处理全局 ESC,交给模态框处理
if (document.body.style.overflow === "hidden") return;
const view = currentViewRef.current;
if (view === "providers") return;
if (isTextEditableTarget(event.target)) return;
event.preventDefault();
setCurrentView("settings");
setCurrentView(view === "skillsDiscovery" ? "skills" : "providers");
};
window.addEventListener("keydown", handleGlobalShortcut);
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleGlobalShortcut);
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
@@ -841,7 +889,7 @@ function App() {
</>
)}
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} visibleApps={visibleApps} />
<div className="flex items-center gap-1 p-1 bg-muted rounded-xl">
<Button
+35 -88
View File
@@ -1,12 +1,16 @@
import type { AppId } from "@/lib/api";
import type { VisibleApps } from "@/types";
import { ProviderIcon } from "@/components/ProviderIcon";
interface AppSwitcherProps {
activeApp: AppId;
onSwitch: (app: AppId) => void;
visibleApps?: VisibleApps;
}
export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
const ALL_APPS: AppId[] = ["claude", "codex", "gemini", "opencode"];
export function AppSwitcher({ activeApp, onSwitch, visibleApps }: AppSwitcherProps) {
const handleSwitch = (app: AppId) => {
if (app === activeApp) return;
onSwitch(app);
@@ -25,95 +29,38 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
opencode: "OpenCode",
};
// Filter apps based on visibility settings (default all visible)
const appsToShow = ALL_APPS.filter((app) => {
if (!visibleApps) return true;
return visibleApps[app];
});
return (
<div className="inline-flex bg-muted rounded-xl p-1 gap-1">
<button
type="button"
onClick={() => handleSwitch("claude")}
className={`group inline-flex items-center gap-2 px-3 h-8 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === "claude"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
}`}
>
<ProviderIcon
icon={appIconName.claude}
name={appDisplayName.claude}
size={iconSize}
className={
activeApp === "claude"
? "text-foreground"
: "text-muted-foreground group-hover:text-foreground transition-colors"
}
/>
<span>{appDisplayName.claude}</span>
</button>
<button
type="button"
onClick={() => handleSwitch("codex")}
className={`group inline-flex items-center gap-2 px-3 h-8 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === "codex"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
}`}
>
<ProviderIcon
icon={appIconName.codex}
name={appDisplayName.codex}
size={iconSize}
className={
activeApp === "codex"
? "text-foreground"
: "text-muted-foreground group-hover:text-foreground transition-colors"
}
/>
<span>{appDisplayName.codex}</span>
</button>
<button
type="button"
onClick={() => handleSwitch("gemini")}
className={`group inline-flex items-center gap-2 px-3 h-8 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === "gemini"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
}`}
>
<ProviderIcon
icon={appIconName.gemini}
name={appDisplayName.gemini}
size={iconSize}
className={
activeApp === "gemini"
? "text-foreground"
: "text-muted-foreground group-hover:text-foreground transition-colors"
}
/>
<span>{appDisplayName.gemini}</span>
</button>
<button
type="button"
onClick={() => handleSwitch("opencode")}
className={`group inline-flex items-center gap-2 px-3 h-8 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === "opencode"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
}`}
>
<ProviderIcon
icon={appIconName.opencode}
name={appDisplayName.opencode}
size={iconSize}
className={
activeApp === "opencode"
? "text-foreground"
: "text-muted-foreground group-hover:text-foreground transition-colors"
}
/>
<span>{appDisplayName.opencode}</span>
</button>
{appsToShow.map((app) => (
<button
key={app}
type="button"
onClick={() => handleSwitch(app)}
className={`group inline-flex items-center gap-2 px-3 h-8 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === app
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
}`}
>
<ProviderIcon
icon={appIconName[app]}
name={appDisplayName[app]}
size={iconSize}
className={
activeApp === app
? "text-foreground"
: "text-muted-foreground group-hover:text-foreground transition-colors"
}
/>
<span>{appDisplayName[app]}</span>
</button>
))}
</div>
);
}
+34
View File
@@ -4,6 +4,7 @@ import { motion, AnimatePresence } from "framer-motion";
import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { isWindows, isLinux } from "@/lib/platform";
import { isTextEditableTarget } from "@/utils/domUtils";
interface FullScreenPanelProps {
isOpen: boolean;
@@ -37,6 +38,39 @@ export const FullScreenPanel: React.FC<FullScreenPanelProps> = ({
};
}, [isOpen]);
// ESC 键关闭面板
const onCloseRef = React.useRef(onClose);
React.useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
React.useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
// 子组件(例如 Radix 的 Select/Dialog/Dropdown)如果已经消费了 ESC,就不要再关闭整个面板
if (event.defaultPrevented) {
return;
}
if (isTextEditableTarget(event.target)) {
return; // 让输入框自己处理 ESC(比如清空、失焦等)
}
event.stopPropagation(); // 阻止事件继续冒泡到 window,避免触发 App.tsx 的全局监听
onCloseRef.current();
}
};
// 使用冒泡阶段监听,让子组件(如 Radix UI)优先处理 ESC
window.addEventListener("keydown", handleKeyDown, false);
return () => {
window.removeEventListener("keydown", handleKeyDown, false);
};
}, [isOpen]);
return createPortal(
<AnimatePresence>
{isOpen && (
@@ -128,6 +128,7 @@ export function EditProviderDialog({
iconColor: provider.iconColor,
};
}, [
open, // 修复:编辑保存后再次打开显示旧数据,依赖 open 确保每次打开时重新读取最新 provider 数据
provider?.id, // 只依赖 ID,provider 对象更新不会触发重新计算
provider?.meta, // 需要依赖 meta 以便正确初始化 testConfig 和 proxyConfig
initialSettingsConfig,
@@ -1,4 +1,4 @@
import { useState, useCallback, useEffect } from "react";
import { useState, useCallback, useEffect, useRef } from "react";
interface UseModelStateProps {
settingsConfig: string;
@@ -19,12 +19,27 @@ export function useModelState({
const [defaultSonnetModel, setDefaultSonnetModel] = useState("");
const [defaultOpusModel, setDefaultOpusModel] = useState("");
const isUserEditingRef = useRef(false);
const lastConfigRef = useRef(settingsConfig);
// 初始化读取:读新键;若缺失,按兼容优先级回退
// Haiku: DEFAULT_HAIKU || SMALL_FAST || MODEL
// Sonnet: DEFAULT_SONNET || MODEL || SMALL_FAST
// Opus: DEFAULT_OPUS || MODEL || SMALL_FAST
// 仅在 settingsConfig 变化时同步一次(表单加载/切换预设时)
useEffect(() => {
if (lastConfigRef.current === settingsConfig) {
return;
}
if (isUserEditingRef.current) {
isUserEditingRef.current = false;
lastConfigRef.current = settingsConfig;
return;
}
lastConfigRef.current = settingsConfig;
try {
const cfg = settingsConfig ? JSON.parse(settingsConfig) : {};
const env = cfg?.env || {};
@@ -71,6 +86,8 @@ export function useModelState({
| "ANTHROPIC_DEFAULT_OPUS_MODEL",
value: string,
) => {
isUserEditingRef.current = true;
if (field === "ANTHROPIC_MODEL") setClaudeModel(value);
if (field === "ANTHROPIC_REASONING_MODEL") setReasoningModel(value);
if (field === "ANTHROPIC_DEFAULT_HAIKU_MODEL")
@@ -0,0 +1,118 @@
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ProviderIcon } from "@/components/ProviderIcon";
import type { SettingsFormState } from "@/hooks/useSettings";
import type { VisibleApps } from "@/types";
import type { AppId } from "@/lib/api";
interface AppVisibilitySettingsProps {
settings: SettingsFormState;
onChange: (updates: Partial<SettingsFormState>) => void;
}
const APP_CONFIG: Array<{
id: AppId;
icon: string;
nameKey: string;
}> = [
{ id: "claude", icon: "claude", nameKey: "apps.claude" },
{ id: "codex", icon: "openai", nameKey: "apps.codex" },
{ id: "gemini", icon: "gemini", nameKey: "apps.gemini" },
{ id: "opencode", icon: "opencode", nameKey: "apps.opencode" },
];
export function AppVisibilitySettings({ settings, onChange }: AppVisibilitySettingsProps) {
const { t } = useTranslation();
const visibleApps: VisibleApps = settings.visibleApps ?? {
claude: true,
codex: true,
gemini: false,
opencode: true,
};
// Count how many apps are currently visible
const visibleCount = Object.values(visibleApps).filter(Boolean).length;
const handleToggle = (appId: AppId) => {
const isCurrentlyVisible = visibleApps[appId];
// Prevent disabling the last visible app
if (isCurrentlyVisible && visibleCount <= 1) return;
onChange({
visibleApps: {
...visibleApps,
[appId]: !isCurrentlyVisible,
},
});
};
return (
<section className="space-y-2">
<header className="space-y-1">
<h3 className="text-sm font-medium">{t("settings.appVisibility.title")}</h3>
<p className="text-xs text-muted-foreground">
{t("settings.appVisibility.description")}
</p>
</header>
<div className="inline-flex gap-1 rounded-md border border-border-default bg-background p-1">
{APP_CONFIG.map((app) => {
const isVisible = visibleApps[app.id];
// Disable button if this is the last visible app
const isDisabled = isVisible && visibleCount <= 1;
return (
<AppButton
key={app.id}
active={isVisible}
disabled={isDisabled}
onClick={() => handleToggle(app.id)}
icon={app.icon}
name={t(app.nameKey)}
>
{t(app.nameKey)}
</AppButton>
);
})}
</div>
</section>
);
}
interface AppButtonProps {
active: boolean;
disabled?: boolean;
onClick: () => void;
icon: string;
name: string;
children: React.ReactNode;
}
function AppButton({
active,
disabled,
onClick,
icon,
name,
children,
}: AppButtonProps) {
return (
<Button
type="button"
onClick={onClick}
disabled={disabled}
size="sm"
variant={active ? "default" : "ghost"}
className={cn(
"w-[90px] gap-1.5",
active
? "shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-muted",
)}
>
<ProviderIcon icon={icon} name={name} size={14} />
{children}
</Button>
);
}
+5
View File
@@ -34,6 +34,7 @@ import { settingsApi } from "@/lib/api";
import { LanguageSettings } from "@/components/settings/LanguageSettings";
import { ThemeSettings } from "@/components/settings/ThemeSettings";
import { WindowSettings } from "@/components/settings/WindowSettings";
import { AppVisibilitySettings } from "@/components/settings/AppVisibilitySettings";
import { DirectorySettings } from "@/components/settings/DirectorySettings";
import { ImportExportSection } from "@/components/settings/ImportExportSection";
import { AboutSection } from "@/components/settings/AboutSection";
@@ -240,6 +241,10 @@ export function SettingsPage({
onChange={(lang) => handleAutoSave({ language: lang })}
/>
<ThemeSettings />
<AppVisibilitySettings
settings={settings}
onChange={handleAutoSave}
/>
<WindowSettings
settings={settings}
onChange={handleAutoSave}
+1 -38
View File
@@ -1,7 +1,7 @@
import { Switch } from "@/components/ui/switch";
import { useTranslation } from "react-i18next";
import type { SettingsFormState } from "@/hooks/useSettings";
import { AppWindow, MonitorUp, Power } from "lucide-react";
import { ToggleRow } from "@/components/ui/toggle-row";
interface WindowSettingsProps {
settings: SettingsFormState;
@@ -58,40 +58,3 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {
</section>
);
}
interface ToggleRowProps {
icon: React.ReactNode;
title: string;
description?: string;
checked: boolean;
onCheckedChange: (value: boolean) => void;
}
function ToggleRow({
icon,
title,
description,
checked,
onCheckedChange,
}: ToggleRowProps) {
return (
<div className="flex items-center justify-between gap-4 rounded-xl border border-border bg-card/50 p-4 transition-colors hover:bg-muted/50">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-background ring-1 ring-border">
{icon}
</div>
<div className="space-y-1">
<p className="text-sm font-medium leading-none">{title}</p>
{description ? (
<p className="text-xs text-muted-foreground">{description}</p>
) : null}
</div>
</div>
<Switch
checked={checked}
onCheckedChange={onCheckedChange}
aria-label={title}
/>
</div>
);
}
+41
View File
@@ -0,0 +1,41 @@
import { Switch } from "@/components/ui/switch";
export interface ToggleRowProps {
icon: React.ReactNode;
title: string;
description?: string;
checked: boolean;
onCheckedChange: (value: boolean) => void;
disabled?: boolean;
}
export function ToggleRow({
icon,
title,
description,
checked,
onCheckedChange,
disabled,
}: ToggleRowProps) {
return (
<div className="flex items-center justify-between gap-4 rounded-xl border border-border bg-card/50 p-4 transition-colors hover:bg-muted/50">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-background ring-1 ring-border">
{icon}
</div>
<div className="space-y-1">
<p className="text-sm font-medium leading-none">{title}</p>
{description ? (
<p className="text-xs text-muted-foreground">{description}</p>
) : null}
</div>
</div>
<Switch
checked={checked}
onCheckedChange={onCheckedChange}
disabled={disabled}
aria-label={title}
/>
</div>
);
}
+9 -1
View File
@@ -272,6 +272,14 @@
"enableClaudePluginIntegrationDescription": "When enabled, the VS Code Claude Code extension provider will switch with this app",
"skipClaudeOnboarding": "Skip Claude Code first-run confirmation",
"skipClaudeOnboardingDescription": "When enabled, Claude Code will skip the first-run confirmation",
"appVisibility": {
"title": "Homepage Display",
"description": "Choose which apps to show on the homepage",
"claudeDesc": "Anthropic Claude Code CLI",
"codexDesc": "OpenAI Codex CLI",
"geminiDesc": "Google Gemini CLI",
"opencodeDesc": "OpenCode CLI"
},
"configDirectoryOverride": "Configuration Directory Override (Advanced)",
"configDirectoryDescription": "When using Claude Code or Codex in environments like WSL, you can manually specify the configuration directory to the one in WSL to keep provider data consistent with the main environment.",
"appConfigDir": "CC Switch Configuration Directory",
@@ -333,7 +341,7 @@
}
},
"apps": {
"claude": "Claude Code",
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
"opencode": "OpenCode"
+9 -1
View File
@@ -272,6 +272,14 @@
"enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します",
"skipClaudeOnboarding": "Claude Code の初回確認をスキップ",
"skipClaudeOnboardingDescription": "オンにすると Claude Code の初回インストール確認をスキップします",
"appVisibility": {
"title": "ホームページ表示",
"description": "ホームページに表示するアプリを選択",
"claudeDesc": "Anthropic Claude Code CLI",
"codexDesc": "OpenAI Codex CLI",
"geminiDesc": "Google Gemini CLI",
"opencodeDesc": "OpenCode CLI"
},
"configDirectoryOverride": "設定ディレクトリの上書き(詳細)",
"configDirectoryDescription": "WSL などで Claude Code や Codex を使う場合、ここで設定ディレクトリを WSL 側に合わせるとデータを揃えられます。",
"appConfigDir": "CC Switch 設定ディレクトリ",
@@ -333,7 +341,7 @@
}
},
"apps": {
"claude": "Claude Code",
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
"opencode": "OpenCode"
+9 -1
View File
@@ -272,6 +272,14 @@
"enableClaudePluginIntegrationDescription": "开启后 Vscode Claude Code 插件的供应商将随本软件切换",
"skipClaudeOnboarding": "跳过 Claude Code 初次安装确认",
"skipClaudeOnboardingDescription": "开启后跳过 Claude Code 初次安装确认",
"appVisibility": {
"title": "主页面显示",
"description": "选择在主页面显示的应用",
"claudeDesc": "Anthropic Claude Code CLI",
"codexDesc": "OpenAI Codex CLI",
"geminiDesc": "Google Gemini CLI",
"opencodeDesc": "OpenCode CLI"
},
"configDirectoryOverride": "配置目录覆盖(高级)",
"configDirectoryDescription": "在 WSL 等环境使用 Claude Code 或 Codex 的时候,可手动指定为 WSL 里的配置目录,供应商数据与主环境保持一致。",
"appConfigDir": "CC Switch 配置目录",
@@ -333,7 +341,7 @@
}
},
"apps": {
"claude": "Claude Code",
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
"opencode": "OpenCode"
+11
View File
@@ -0,0 +1,11 @@
export function isTextEditableTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
const tagName = target.tagName;
return (
tagName === "INPUT" ||
tagName === "TEXTAREA" ||
tagName === "SELECT" ||
target.isContentEditable
);
}