设置界面支持版本控制和github账号管理

This commit is contained in:
itpkcn@gmail.com
2026-03-21 11:33:48 +08:00
parent e801f041a0
commit 62fab2c3f2
22 changed files with 1595 additions and 4 deletions

View File

@@ -32,7 +32,7 @@ sacp-tokio = "11.0.0-alpha.1"
tokio = { version = "1", features = ["process", "io-util", "sync", "macros", "rt"] } tokio = { version = "1", features = ["process", "io-util", "sync", "macros", "rt"] }
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
futures = "0.3" futures = "0.3"
reqwest = { version = "0.12", features = ["stream"] } reqwest = { version = "0.12", features = ["stream", "json"] }
flate2 = "1" flate2 = "1"
bzip2 = "0.5" bzip2 = "0.5"
tar = "0.4" tar = "0.4"

View File

@@ -5,4 +5,5 @@ pub mod folders;
pub mod mcp; pub mod mcp;
pub mod system_settings; pub mod system_settings;
pub mod terminal; pub mod terminal;
pub mod version_control;
pub mod windows; pub mod windows;

View File

@@ -0,0 +1,288 @@
use serde::Deserialize;
use tauri::State;
use crate::app_error::AppCommandError;
use crate::db::service::app_metadata_service;
use crate::db::AppDatabase;
use crate::models::{
GitDetectResult, GitHubAccountsSettings, GitHubTokenValidation, GitSettings,
};
const GIT_SETTINGS_KEY: &str = "git_settings";
const GITHUB_ACCOUNTS_KEY: &str = "github_accounts";
// ---------------------------------------------------------------------------
// Git detection
// ---------------------------------------------------------------------------
async fn run_git_version(git_path: &str) -> Result<GitDetectResult, AppCommandError> {
let output = crate::process::tokio_command(git_path)
.arg("--version")
.output()
.await
.map_err(|_| {
AppCommandError::not_found(format!("Cannot execute git at: {git_path}"))
})?;
if !output.status.success() {
return Ok(GitDetectResult {
installed: false,
version: None,
path: Some(git_path.to_string()),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
let version = stdout
.trim()
.strip_prefix("git version ")
.unwrap_or(stdout.trim())
.to_string();
Ok(GitDetectResult {
installed: true,
version: Some(version),
path: Some(git_path.to_string()),
})
}
async fn detect_git_path() -> Option<String> {
let which_cmd = if cfg!(target_os = "windows") {
"where"
} else {
"which"
};
let output = crate::process::tokio_command(which_cmd)
.arg("git")
.output()
.await
.ok()?;
if !output.status.success() {
return None;
}
let path = String::from_utf8_lossy(&output.stdout)
.lines()
.next()?
.trim()
.to_string();
if path.is_empty() {
None
} else {
Some(path)
}
}
#[tauri::command]
pub async fn detect_git(
db: State<'_, AppDatabase>,
) -> Result<GitDetectResult, AppCommandError> {
// Check if there's a custom path configured
let settings = load_git_settings(&db.conn).await?;
if let Some(custom) = &settings.custom_path {
let trimmed = custom.trim();
if !trimmed.is_empty() {
return run_git_version(trimmed).await;
}
}
// Auto-detect
if let Some(path) = detect_git_path().await {
return run_git_version(&path).await;
}
// Fallback: try "git" directly (might be in PATH but `which` failed)
match run_git_version("git").await {
Ok(result) if result.installed => Ok(result),
_ => Ok(GitDetectResult {
installed: false,
version: None,
path: None,
}),
}
}
#[tauri::command]
pub async fn test_git_path(path: String) -> Result<GitDetectResult, AppCommandError> {
let trimmed = path.trim();
if trimmed.is_empty() {
return Err(AppCommandError::invalid_input("Git path cannot be empty"));
}
run_git_version(trimmed).await
}
// ---------------------------------------------------------------------------
// Git settings
// ---------------------------------------------------------------------------
async fn load_git_settings(
conn: &sea_orm::DatabaseConnection,
) -> Result<GitSettings, AppCommandError> {
let raw = app_metadata_service::get_value(conn, GIT_SETTINGS_KEY)
.await
.map_err(AppCommandError::from)?;
match raw {
Some(raw) => serde_json::from_str::<GitSettings>(&raw).map_err(|e| {
AppCommandError::configuration_invalid("Failed to parse stored git settings")
.with_detail(e.to_string())
}),
None => Ok(GitSettings::default()),
}
}
#[tauri::command]
pub async fn get_git_settings(
db: State<'_, AppDatabase>,
) -> Result<GitSettings, AppCommandError> {
load_git_settings(&db.conn).await
}
#[tauri::command]
pub async fn update_git_settings(
settings: GitSettings,
db: State<'_, AppDatabase>,
) -> Result<GitSettings, AppCommandError> {
let serialized = serde_json::to_string(&settings).map_err(|e| {
AppCommandError::invalid_input("Failed to serialize git settings")
.with_detail(e.to_string())
})?;
app_metadata_service::upsert_value(&db.conn, GIT_SETTINGS_KEY, &serialized)
.await
.map_err(AppCommandError::from)?;
Ok(settings)
}
// ---------------------------------------------------------------------------
// GitHub accounts
// ---------------------------------------------------------------------------
async fn load_github_accounts(
conn: &sea_orm::DatabaseConnection,
) -> Result<GitHubAccountsSettings, AppCommandError> {
let raw = app_metadata_service::get_value(conn, GITHUB_ACCOUNTS_KEY)
.await
.map_err(AppCommandError::from)?;
match raw {
Some(raw) => serde_json::from_str::<GitHubAccountsSettings>(&raw).map_err(|e| {
AppCommandError::configuration_invalid("Failed to parse stored GitHub accounts")
.with_detail(e.to_string())
}),
None => Ok(GitHubAccountsSettings::default()),
}
}
#[tauri::command]
pub async fn get_github_accounts(
db: State<'_, AppDatabase>,
) -> Result<GitHubAccountsSettings, AppCommandError> {
load_github_accounts(&db.conn).await
}
#[tauri::command]
pub async fn update_github_accounts(
settings: GitHubAccountsSettings,
db: State<'_, AppDatabase>,
) -> Result<GitHubAccountsSettings, AppCommandError> {
let serialized = serde_json::to_string(&settings).map_err(|e| {
AppCommandError::invalid_input("Failed to serialize GitHub accounts")
.with_detail(e.to_string())
})?;
app_metadata_service::upsert_value(&db.conn, GITHUB_ACCOUNTS_KEY, &serialized)
.await
.map_err(AppCommandError::from)?;
Ok(settings)
}
#[derive(Debug, Deserialize)]
struct GitHubUserResponse {
login: String,
avatar_url: Option<String>,
}
#[tauri::command]
pub async fn validate_github_token(
server_url: String,
token: String,
) -> Result<GitHubTokenValidation, AppCommandError> {
let trimmed_url = server_url.trim().trim_end_matches('/');
let trimmed_token = token.trim();
if trimmed_token.is_empty() {
return Err(AppCommandError::invalid_input("Token cannot be empty"));
}
// Build API URL: github.com uses api.github.com, enterprise uses {url}/api/v3
let api_url = if trimmed_url.is_empty()
|| trimmed_url == "https://github.com"
|| trimmed_url == "http://github.com"
{
"https://api.github.com/user".to_string()
} else {
format!("{trimmed_url}/api/v3/user")
};
let response = reqwest::Client::new()
.get(&api_url)
.header("Authorization", format!("Bearer {trimmed_token}"))
.header("User-Agent", "codeg")
.header("Accept", "application/vnd.github+json")
.send()
.await
.map_err(|e| AppCommandError::network("Failed to connect to GitHub API").with_detail(e.to_string()))?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
let message = if status == 401 {
"Invalid or expired token".to_string()
} else {
format!("GitHub API returned status {status}: {body}")
};
return Ok(GitHubTokenValidation {
success: false,
username: None,
scopes: vec![],
avatar_url: None,
message: Some(message),
});
}
// Parse scopes from x-oauth-scopes header
let scopes: Vec<String> = response
.headers()
.get("x-oauth-scopes")
.and_then(|v| v.to_str().ok())
.map(|s| {
s.split(',')
.map(|scope| scope.trim().to_string())
.filter(|scope| !scope.is_empty())
.collect()
})
.unwrap_or_default();
let user = response
.json::<GitHubUserResponse>()
.await
.map_err(|e| {
AppCommandError::network("Failed to parse GitHub API response")
.with_detail(e.to_string())
})?;
Ok(GitHubTokenValidation {
success: true,
username: Some(user.login),
scopes,
avatar_url: user.avatar_url,
message: None,
})
}

View File

@@ -13,7 +13,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use acp::manager::ConnectionManager; use acp::manager::ConnectionManager;
use commands::{ use commands::{
acp as acp_commands, conversations, folder_commands, folders, mcp as mcp_commands, acp as acp_commands, conversations, folder_commands, folders, mcp as mcp_commands,
system_settings, terminal as terminal_commands, windows, system_settings, terminal as terminal_commands, version_control, windows,
}; };
use tauri::Manager; use tauri::Manager;
use terminal::manager::TerminalManager; use terminal::manager::TerminalManager;
@@ -264,6 +264,13 @@ pub fn run() {
system_settings::update_system_proxy_settings, system_settings::update_system_proxy_settings,
system_settings::get_system_language_settings, system_settings::get_system_language_settings,
system_settings::update_system_language_settings, system_settings::update_system_language_settings,
version_control::detect_git,
version_control::test_git_path,
version_control::get_git_settings,
version_control::update_git_settings,
version_control::get_github_accounts,
version_control::validate_github_token,
version_control::update_github_accounts,
acp_commands::acp_preflight, acp_commands::acp_preflight,
acp_commands::acp_connect, acp_commands::acp_connect,
acp_commands::acp_prompt, acp_commands::acp_prompt,

View File

@@ -12,4 +12,7 @@ pub use conversation::{
}; };
pub use folder::{FolderCommandInfo, FolderDetail, FolderHistoryEntry, OpenedConversation}; pub use folder::{FolderCommandInfo, FolderDetail, FolderHistoryEntry, OpenedConversation};
pub use message::{ContentBlock, MessageRole, MessageTurn, TurnRole, TurnUsage, UnifiedMessage}; pub use message::{ContentBlock, MessageRole, MessageTurn, TurnRole, TurnUsage, UnifiedMessage};
pub use system::{SystemLanguageSettings, SystemProxySettings}; pub use system::{
GitDetectResult, GitHubAccountsSettings, GitHubTokenValidation, GitSettings,
SystemLanguageSettings, SystemProxySettings,
};

View File

@@ -36,3 +36,45 @@ pub struct SystemLanguageSettings {
pub mode: LanguageMode, pub mode: LanguageMode,
pub language: AppLocale, pub language: AppLocale,
} }
// --- Version Control ---
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitDetectResult {
pub installed: bool,
pub version: Option<String>,
pub path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct GitSettings {
pub custom_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitHubAccount {
pub id: String,
pub server_url: String,
pub username: String,
pub token: String,
pub scopes: Vec<String>,
pub avatar_url: Option<String>,
pub is_default: bool,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct GitHubAccountsSettings {
pub accounts: Vec<GitHubAccount>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitHubTokenValidation {
pub success: bool,
pub username: Option<String>,
pub scopes: Vec<String>,
pub avatar_url: Option<String>,
pub message: Option<String>,
}

View File

@@ -0,0 +1,5 @@
import { VersionControlSettings } from "@/components/settings/version-control-settings"
export default function SettingsVersionControlPage() {
return <VersionControlSettings />
}

View File

@@ -0,0 +1,179 @@
"use client"
import { useCallback, useState } from "react"
import { Eye, EyeOff, Loader2 } from "lucide-react"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { validateGitHubToken } from "@/lib/tauri"
import type { GitHubAccount } from "@/lib/types"
interface AddGitHubAccountDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onAccountAdded: (account: GitHubAccount) => void
isFirstAccount: boolean
}
export function AddGitHubAccountDialog({
open,
onOpenChange,
onAccountAdded,
isFirstAccount,
}: AddGitHubAccountDialogProps) {
const t = useTranslations("VersionControlSettings")
const [serverUrl, setServerUrl] = useState("https://github.com")
const [token, setToken] = useState("")
const [showToken, setShowToken] = useState(false)
const [validating, setValidating] = useState(false)
const [error, setError] = useState<string | null>(null)
const resetForm = useCallback(() => {
setServerUrl("https://github.com")
setToken("")
setShowToken(false)
setValidating(false)
setError(null)
}, [])
const handleOpenChange = useCallback(
(nextOpen: boolean) => {
if (!nextOpen) {
resetForm()
}
onOpenChange(nextOpen)
},
[onOpenChange, resetForm]
)
const handleSubmit = useCallback(async () => {
const trimmedToken = token.trim()
if (!trimmedToken) {
setError(t("addFailed", { message: "Token is required" }))
return
}
setValidating(true)
setError(null)
try {
const result = await validateGitHubToken(serverUrl.trim(), trimmedToken)
if (!result.success) {
setError(
t("addFailed", { message: result.message ?? "Validation failed" })
)
return
}
const account: GitHubAccount = {
id: crypto.randomUUID(),
server_url: serverUrl.trim() || "https://github.com",
username: result.username ?? "unknown",
token: trimmedToken,
scopes: result.scopes,
avatar_url: result.avatar_url,
is_default: isFirstAccount,
created_at: new Date().toISOString(),
}
onAccountAdded(account)
handleOpenChange(false)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
setError(t("addFailed", { message }))
} finally {
setValidating(false)
}
}, [serverUrl, token, isFirstAccount, onAccountAdded, handleOpenChange, t])
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t("addAccount")}</DialogTitle>
<DialogDescription>{t("githubDescription")}</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
{t("serverUrl")}
</label>
<Input
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
placeholder={t("serverUrlPlaceholder")}
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
{t("token")}
</label>
<div className="relative">
<Input
type={showToken ? "text" : "password"}
value={token}
onChange={(e) => {
setToken(e.target.value)
setError(null)
}}
placeholder={t("tokenPlaceholder")}
className="pr-9"
/>
<Button
type="button"
variant="ghost"
size="xs"
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0"
onClick={() => setShowToken(!showToken)}
tabIndex={-1}
>
{showToken ? (
<EyeOff className="h-3.5 w-3.5" />
) : (
<Eye className="h-3.5 w-3.5" />
)}
</Button>
</div>
<p className="text-[11px] text-muted-foreground">
{t("tokenHint")}
</p>
</div>
{error && (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
{error}
</div>
)}
</div>
<DialogFooter>
<Button
onClick={handleSubmit}
disabled={validating || !token.trim()}
>
{validating ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("validating")}
</>
) : (
t("validateAndAdd")
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -4,6 +4,7 @@ import { useCallback, type ComponentType, type ReactNode } from "react"
import { import {
Bot, Bot,
BookOpenText, BookOpenText,
GitBranch,
Keyboard, Keyboard,
Palette, Palette,
PlugZap, PlugZap,
@@ -19,7 +20,14 @@ import { AppTitleBar } from "@/components/layout/app-title-bar"
interface SettingsNavItem { interface SettingsNavItem {
href: string href: string
labelKey: "appearance" | "agents" | "mcp" | "skills" | "shortcuts" | "system" labelKey:
| "appearance"
| "agents"
| "mcp"
| "skills"
| "shortcuts"
| "version_control"
| "system"
icon: ComponentType<{ className?: string }> icon: ComponentType<{ className?: string }>
} }
@@ -49,6 +57,11 @@ const SETTINGS_NAV_ITEMS: SettingsNavItem[] = [
labelKey: "shortcuts", labelKey: "shortcuts",
icon: Keyboard, icon: Keyboard,
}, },
{
href: "/settings/version-control",
labelKey: "version_control",
icon: GitBranch,
},
{ {
href: "/settings/system", href: "/settings/system",
labelKey: "system", labelKey: "system",

View File

@@ -0,0 +1,477 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import {
CheckCircle2,
GitBranch,
Github,
Loader2,
Save,
Trash2,
XCircle,
} from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import {
detectGit,
getGitSettings,
updateGitSettings,
testGitPath,
getGitHubAccounts,
updateGitHubAccounts,
validateGitHubToken,
} from "@/lib/tauri"
import type {
GitDetectResult,
GitHubAccount,
GitHubAccountsSettings,
} from "@/lib/types"
import { AddGitHubAccountDialog } from "./add-github-account-dialog"
export function VersionControlSettings() {
const t = useTranslations("VersionControlSettings")
const [loading, setLoading] = useState(true)
const [gitInfo, setGitInfo] = useState<GitDetectResult | null>(null)
const [customPath, setCustomPath] = useState("")
const [savingGit, setSavingGit] = useState(false)
const [testingGit, setTestingGit] = useState(false)
const [testResult, setTestResult] = useState<GitDetectResult | null>(null)
const [accounts, setAccounts] = useState<GitHubAccountsSettings>({
accounts: [],
})
const [addDialogOpen, setAddDialogOpen] = useState(false)
const [testingAccountId, setTestingAccountId] = useState<string | null>(null)
const [removeTarget, setRemoveTarget] = useState<GitHubAccount | null>(null)
const loadData = useCallback(async () => {
setLoading(true)
try {
const [git, settings, ghAccounts] = await Promise.all([
detectGit(),
getGitSettings(),
getGitHubAccounts(),
])
setGitInfo(git)
setCustomPath(settings.custom_path ?? "")
setAccounts(ghAccounts)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("loadFailed", { message }))
} finally {
setLoading(false)
}
}, [t])
useEffect(() => {
loadData().catch(console.error)
}, [loadData])
const handleTestGit = useCallback(async () => {
const pathToTest = customPath.trim() || "git"
setTestingGit(true)
setTestResult(null)
try {
const result = await testGitPath(pathToTest)
setTestResult(result)
if (result.installed) {
toast.success(t("testSuccess"))
} else {
toast.error(t("testFailed", { message: "not a valid git executable" }))
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("testFailed", { message }))
} finally {
setTestingGit(false)
}
}, [customPath, t])
const handleSaveGit = useCallback(async () => {
setSavingGit(true)
try {
await updateGitSettings({
custom_path: customPath.trim() || null,
})
const git = await detectGit()
setGitInfo(git)
toast.success(t("saveSuccess"))
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("saveFailed", { message }))
} finally {
setSavingGit(false)
}
}, [customPath, t])
const handleAccountAdded = useCallback(
async (account: GitHubAccount) => {
const updated: GitHubAccountsSettings = {
accounts: [
...accounts.accounts.map((a) =>
account.is_default ? { ...a, is_default: false } : a
),
account,
],
}
try {
const saved = await updateGitHubAccounts(updated)
setAccounts(saved)
toast.success(t("addSuccess", { username: account.username }))
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("addFailed", { message }))
}
},
[accounts, t]
)
const handleTestConnection = useCallback(
async (account: GitHubAccount) => {
setTestingAccountId(account.id)
try {
const result = await validateGitHubToken(
account.server_url,
account.token
)
if (result.success) {
toast.success(t("connectionSuccess"))
} else {
toast.error(
t("connectionFailed", {
message: result.message ?? "Unknown error",
})
)
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("connectionFailed", { message }))
} finally {
setTestingAccountId(null)
}
},
[t]
)
const handleSetDefault = useCallback(
async (accountId: string) => {
const updated: GitHubAccountsSettings = {
accounts: accounts.accounts.map((a) => ({
...a,
is_default: a.id === accountId,
})),
}
try {
const saved = await updateGitHubAccounts(updated)
setAccounts(saved)
toast.success(t("defaultSet"))
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(message)
}
},
[accounts, t]
)
const handleRemoveAccount = useCallback(async () => {
if (!removeTarget) return
const updated: GitHubAccountsSettings = {
accounts: accounts.accounts.filter((a) => a.id !== removeTarget.id),
}
try {
const saved = await updateGitHubAccounts(updated)
setAccounts(saved)
toast.success(t("removeSuccess"))
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(message)
} finally {
setRemoveTarget(null)
}
}, [accounts, removeTarget, t])
if (loading) {
return (
<div className="h-full flex items-center justify-center text-sm text-muted-foreground gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
{t("loading")}
</div>
)
}
return (
<div className="h-full overflow-auto">
<div className="w-full space-y-4">
<section className="space-y-1">
<h1 className="text-sm font-semibold">{t("sectionTitle")}</h1>
<p className="text-xs text-muted-foreground">
{t("sectionDescription")}
</p>
</section>
{/* Git Configuration */}
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">{t("gitTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
{t("gitDescription")}
</p>
{/* Git status */}
<div className="rounded-md border bg-muted/20 px-3 py-3 text-xs space-y-2">
<div className="flex items-center gap-2">
{gitInfo?.installed ? (
<>
<CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
<span className="text-green-600 dark:text-green-400 font-medium">
{t("gitDetected")}
</span>
</>
) : (
<>
<XCircle className="h-3.5 w-3.5 text-red-500" />
<span className="text-red-600 dark:text-red-400 font-medium">
{t("gitNotFound")}
</span>
</>
)}
</div>
{gitInfo?.version && (
<p className="text-muted-foreground">
{t("gitVersion")}: {gitInfo.version}
</p>
)}
{gitInfo?.path && (
<p className="text-muted-foreground">
{t("gitPath")}: {gitInfo.path}
</p>
)}
</div>
{/* Custom path */}
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
{t("customGitPath")}
</label>
<div className="flex gap-2">
<Input
value={customPath}
onChange={(e) => {
setCustomPath(e.target.value)
setTestResult(null)
}}
placeholder={t("customGitPathPlaceholder")}
className="flex-1"
/>
<Button
size="sm"
variant="outline"
onClick={handleTestGit}
disabled={testingGit}
>
{testingGit ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("testing")}
</>
) : (
t("test")
)}
</Button>
</div>
<p className="text-[11px] text-muted-foreground">
{t("customGitPathHint")}
</p>
{testResult && (
<div
className={`flex items-center gap-1.5 text-xs ${
testResult.installed
? "text-green-600 dark:text-green-400"
: "text-red-600 dark:text-red-400"
}`}
>
{testResult.installed ? (
<CheckCircle2 className="h-3 w-3" />
) : (
<XCircle className="h-3 w-3" />
)}
{testResult.installed
? `${t("testSuccess")} (${testResult.version})`
: t("testFailed", { message: "invalid" })}
</div>
)}
</div>
<div className="flex justify-end">
<Button size="sm" onClick={handleSaveGit} disabled={savingGit}>
{savingGit ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("saving")}
</>
) : (
<>
<Save className="h-3.5 w-3.5" />
{t("save")}
</>
)}
</Button>
</div>
</section>
{/* GitHub Accounts */}
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<Github className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">{t("githubTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
{t("githubDescription")}
</p>
{/* Account list */}
{accounts.accounts.length === 0 ? (
<div className="rounded-md border border-dashed bg-muted/10 px-4 py-6 text-center text-xs text-muted-foreground">
{t("noAccounts")}
</div>
) : (
<div className="space-y-2">
{accounts.accounts.map((account) => (
<div
key={account.id}
className="flex items-center gap-3 rounded-lg border bg-muted/10 px-3 py-2.5"
>
{/* Avatar */}
{account.avatar_url ? (
<img
src={account.avatar_url}
alt={account.username}
className="h-8 w-8 rounded-full"
/>
) : (
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center text-xs font-medium">
{account.username[0]?.toUpperCase()}
</div>
)}
{/* Info */}
<div className="flex-1 min-w-0 space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">
{account.username}
</span>
{account.is_default && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
{t("defaultLabel")}
</Badge>
)}
</div>
<div className="flex items-center gap-2 text-[11px] text-muted-foreground">
<span className="truncate">{account.server_url}</span>
{account.scopes.length > 0 && (
<>
<span>·</span>
<span className="truncate">
{account.scopes.join(", ")}
</span>
</>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-1 shrink-0">
<Button
size="xs"
variant="ghost"
onClick={() => handleTestConnection(account)}
disabled={testingAccountId === account.id}
>
{testingAccountId === account.id ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
t("testConnection")
)}
</Button>
{!account.is_default && (
<Button
size="xs"
variant="ghost"
onClick={() => handleSetDefault(account.id)}
>
{t("setDefault")}
</Button>
)}
<Button
size="xs"
variant="ghost"
className="text-destructive hover:text-destructive"
onClick={() => setRemoveTarget(account)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
))}
</div>
)}
<div className="flex justify-end">
<Button size="sm" onClick={() => setAddDialogOpen(true)}>
{t("addAccount")}
</Button>
</div>
</section>
</div>
{/* Add Account Dialog */}
<AddGitHubAccountDialog
open={addDialogOpen}
onOpenChange={setAddDialogOpen}
onAccountAdded={handleAccountAdded}
isFirstAccount={accounts.accounts.length === 0}
/>
{/* Remove Confirmation */}
<AlertDialog
open={!!removeTarget}
onOpenChange={(open) => !open && setRemoveTarget(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("removeConfirmTitle")}</AlertDialogTitle>
<AlertDialogDescription>
{t("removeConfirmMessage", {
username: removeTarget?.username ?? "",
})}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("removeCancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleRemoveAccount}>
{t("removeConfirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}

View File

@@ -66,6 +66,7 @@
"mcp": "MCP", "mcp": "MCP",
"skills": "Skills", "skills": "Skills",
"shortcuts": "الاختصارات", "shortcuts": "الاختصارات",
"version_control": "التحكم بالإصدارات",
"system": "النظام" "system": "النظام"
} }
}, },
@@ -128,6 +129,55 @@
"unknown": "فشل التحديث. يرجى المحاولة مرة أخرى لاحقًا." "unknown": "فشل التحديث. يرجى المحاولة مرة أخرى لاحقًا."
} }
}, },
"VersionControlSettings": {
"loading": "جارٍ التحميل...",
"sectionTitle": "التحكم بالإصدارات",
"sectionDescription": "تكوين ملف Git التنفيذي وإدارة حسابات GitHub.",
"gitTitle": "إعدادات Git",
"gitDescription": "تكوين ملف Git التنفيذي المستخدم بواسطة التطبيق.",
"gitDetected": "تم اكتشاف Git",
"gitNotFound": "لم يتم العثور على Git في النظام",
"gitVersion": "الإصدار",
"gitPath": "المسار",
"customGitPath": "مسار Git مخصص",
"customGitPathPlaceholder": "/usr/bin/git",
"customGitPathHint": "اتركه فارغًا لاستخدام المسار المكتشف تلقائيًا.",
"test": "اختبار",
"testing": "جارٍ الاختبار...",
"testSuccess": "ملف Git التنفيذي صالح.",
"testFailed": "فشل اختبار Git: {message}",
"save": "حفظ",
"saving": "جارٍ الحفظ...",
"saveSuccess": "تم حفظ إعدادات Git.",
"saveFailed": "فشل الحفظ: {message}",
"githubTitle": "حسابات GitHub",
"githubDescription": "إدارة حسابات GitHub للمصادقة. يتم تخزين الرموز محليًا.",
"noAccounts": "لا توجد حسابات GitHub مكوّنة.",
"addAccount": "إضافة حساب",
"serverUrl": "عنوان الخادم",
"serverUrlPlaceholder": "https://github.com",
"token": "رمز الوصول الشخصي",
"tokenPlaceholder": "ghp_xxxxxxxxxxxx",
"tokenHint": "أنشئ رمزًا في GitHub → Settings → Developer settings → Personal access tokens.",
"validateAndAdd": "التحقق والإضافة",
"validating": "جارٍ التحقق...",
"addSuccess": "تمت إضافة الحساب {username} بنجاح.",
"addFailed": "فشل إضافة الحساب: {message}",
"testConnection": "اختبار",
"connectionSuccess": "نجح الاتصال.",
"connectionFailed": "فشل الاتصال: {message}",
"setDefault": "تعيين كافتراضي",
"defaultLabel": "افتراضي",
"defaultSet": "تم تحديث الحساب الافتراضي.",
"removeAccount": "إزالة",
"removeConfirmTitle": "إزالة الحساب",
"removeConfirmMessage": "هل أنت متأكد من إزالة الحساب \"{username}\"؟",
"removeConfirm": "إزالة",
"removeCancel": "إلغاء",
"removeSuccess": "تمت إزالة الحساب.",
"scopes": "النطاقات",
"loadFailed": "فشل تحميل الإعدادات: {message}"
},
"ShortcutSettings": { "ShortcutSettings": {
"sectionTitle": "الاختصارات", "sectionTitle": "الاختصارات",
"resetDefault": "استعادة الإعدادات الافتراضية", "resetDefault": "استعادة الإعدادات الافتراضية",

View File

@@ -66,6 +66,7 @@
"mcp": "MCP", "mcp": "MCP",
"skills": "Skills", "skills": "Skills",
"shortcuts": "Kurzbefehle", "shortcuts": "Kurzbefehle",
"version_control": "Versionskontrolle",
"system": "Systemeinstellungen" "system": "Systemeinstellungen"
} }
}, },
@@ -128,6 +129,55 @@
"unknown": "Update fehlgeschlagen. Bitte später erneut versuchen." "unknown": "Update fehlgeschlagen. Bitte später erneut versuchen."
} }
}, },
"VersionControlSettings": {
"loading": "Laden...",
"sectionTitle": "Versionskontrolle",
"sectionDescription": "Git-Programm konfigurieren und GitHub-Konten verwalten.",
"gitTitle": "Git-Konfiguration",
"gitDescription": "Konfigurieren Sie das von der Anwendung verwendete Git-Programm.",
"gitDetected": "Git erkannt",
"gitNotFound": "Git wurde nicht gefunden",
"gitVersion": "Version",
"gitPath": "Pfad",
"customGitPath": "Benutzerdefinierter Git-Pfad",
"customGitPathPlaceholder": "/usr/bin/git",
"customGitPathHint": "Leer lassen, um den automatisch erkannten Pfad zu verwenden.",
"test": "Testen",
"testing": "Teste...",
"testSuccess": "Git-Programm ist gültig.",
"testFailed": "Git-Test fehlgeschlagen: {message}",
"save": "Speichern",
"saving": "Speichern...",
"saveSuccess": "Git-Einstellungen gespeichert.",
"saveFailed": "Speichern fehlgeschlagen: {message}",
"githubTitle": "GitHub-Konten",
"githubDescription": "GitHub-Konten für die Authentifizierung verwalten. Token werden lokal gespeichert.",
"noAccounts": "Keine GitHub-Konten konfiguriert.",
"addAccount": "Konto hinzufügen",
"serverUrl": "Server-URL",
"serverUrlPlaceholder": "https://github.com",
"token": "Persönlicher Zugriffstoken",
"tokenPlaceholder": "ghp_xxxxxxxxxxxx",
"tokenHint": "Erstellen Sie einen Token unter GitHub → Settings → Developer settings → Personal access tokens.",
"validateAndAdd": "Validieren & hinzufügen",
"validating": "Validiere...",
"addSuccess": "Konto {username} erfolgreich hinzugefügt.",
"addFailed": "Konto konnte nicht hinzugefügt werden: {message}",
"testConnection": "Testen",
"connectionSuccess": "Verbindung erfolgreich.",
"connectionFailed": "Verbindung fehlgeschlagen: {message}",
"setDefault": "Als Standard festlegen",
"defaultLabel": "Standard",
"defaultSet": "Standardkonto aktualisiert.",
"removeAccount": "Entfernen",
"removeConfirmTitle": "Konto entfernen",
"removeConfirmMessage": "Möchten Sie das Konto \"{username}\" wirklich entfernen?",
"removeConfirm": "Entfernen",
"removeCancel": "Abbrechen",
"removeSuccess": "Konto entfernt.",
"scopes": "Berechtigungen",
"loadFailed": "Einstellungen konnten nicht geladen werden: {message}"
},
"ShortcutSettings": { "ShortcutSettings": {
"sectionTitle": "Kurzbefehle", "sectionTitle": "Kurzbefehle",
"resetDefault": "Standardwerte zurücksetzen", "resetDefault": "Standardwerte zurücksetzen",

View File

@@ -66,6 +66,7 @@
"mcp": "MCP", "mcp": "MCP",
"skills": "Skills", "skills": "Skills",
"shortcuts": "Shortcuts", "shortcuts": "Shortcuts",
"version_control": "Version Control",
"system": "System" "system": "System"
} }
}, },
@@ -128,6 +129,55 @@
"unknown": "Update failed. Please try again later." "unknown": "Update failed. Please try again later."
} }
}, },
"VersionControlSettings": {
"loading": "Loading...",
"sectionTitle": "Version Control",
"sectionDescription": "Configure Git executable and manage GitHub accounts.",
"gitTitle": "Git Configuration",
"gitDescription": "Configure the Git executable used by the application.",
"gitDetected": "Git detected",
"gitNotFound": "Git not found on this system",
"gitVersion": "Version",
"gitPath": "Path",
"customGitPath": "Custom Git Path",
"customGitPathPlaceholder": "/usr/bin/git",
"customGitPathHint": "Leave empty to use the auto-detected path.",
"test": "Test",
"testing": "Testing...",
"testSuccess": "Git executable is valid.",
"testFailed": "Git test failed: {message}",
"save": "Save",
"saving": "Saving...",
"saveSuccess": "Git settings saved.",
"saveFailed": "Failed to save: {message}",
"githubTitle": "GitHub Accounts",
"githubDescription": "Manage GitHub accounts for authentication. Tokens are stored locally.",
"noAccounts": "No GitHub accounts configured.",
"addAccount": "Add Account",
"serverUrl": "Server URL",
"serverUrlPlaceholder": "https://github.com",
"token": "Personal Access Token",
"tokenPlaceholder": "ghp_xxxxxxxxxxxx",
"tokenHint": "Generate a token at GitHub → Settings → Developer settings → Personal access tokens.",
"validateAndAdd": "Validate & Add",
"validating": "Validating...",
"addSuccess": "Account {username} added successfully.",
"addFailed": "Failed to add account: {message}",
"testConnection": "Test",
"connectionSuccess": "Connection successful.",
"connectionFailed": "Connection failed: {message}",
"setDefault": "Set Default",
"defaultLabel": "Default",
"defaultSet": "Default account updated.",
"removeAccount": "Remove",
"removeConfirmTitle": "Remove Account",
"removeConfirmMessage": "Are you sure you want to remove the account \"{username}\"?",
"removeConfirm": "Remove",
"removeCancel": "Cancel",
"removeSuccess": "Account removed.",
"scopes": "Scopes",
"loadFailed": "Failed to load settings: {message}"
},
"ShortcutSettings": { "ShortcutSettings": {
"sectionTitle": "Shortcuts", "sectionTitle": "Shortcuts",
"resetDefault": "Reset defaults", "resetDefault": "Reset defaults",

View File

@@ -66,6 +66,7 @@
"mcp": "MCP", "mcp": "MCP",
"skills": "Skills", "skills": "Skills",
"shortcuts": "Atajos", "shortcuts": "Atajos",
"version_control": "Control de versiones",
"system": "Sistema" "system": "Sistema"
} }
}, },
@@ -128,6 +129,55 @@
"unknown": "La actualización falló. Inténtalo más tarde." "unknown": "La actualización falló. Inténtalo más tarde."
} }
}, },
"VersionControlSettings": {
"loading": "Cargando...",
"sectionTitle": "Control de versiones",
"sectionDescription": "Configura el ejecutable de Git y gestiona las cuentas de GitHub.",
"gitTitle": "Configuración de Git",
"gitDescription": "Configura el ejecutable de Git utilizado por la aplicación.",
"gitDetected": "Git detectado",
"gitNotFound": "Git no encontrado en el sistema",
"gitVersion": "Versión",
"gitPath": "Ruta",
"customGitPath": "Ruta personalizada de Git",
"customGitPathPlaceholder": "/usr/bin/git",
"customGitPathHint": "Dejar vacío para usar la ruta detectada automáticamente.",
"test": "Probar",
"testing": "Probando...",
"testSuccess": "El ejecutable de Git es válido.",
"testFailed": "Prueba de Git fallida: {message}",
"save": "Guardar",
"saving": "Guardando...",
"saveSuccess": "Configuración de Git guardada.",
"saveFailed": "Error al guardar: {message}",
"githubTitle": "Cuentas de GitHub",
"githubDescription": "Gestiona las cuentas de GitHub para autenticación. Los tokens se almacenan localmente.",
"noAccounts": "No hay cuentas de GitHub configuradas.",
"addAccount": "Añadir cuenta",
"serverUrl": "URL del servidor",
"serverUrlPlaceholder": "https://github.com",
"token": "Token de acceso personal",
"tokenPlaceholder": "ghp_xxxxxxxxxxxx",
"tokenHint": "Genera un token en GitHub → Settings → Developer settings → Personal access tokens.",
"validateAndAdd": "Validar y añadir",
"validating": "Validando...",
"addSuccess": "Cuenta {username} añadida correctamente.",
"addFailed": "Error al añadir cuenta: {message}",
"testConnection": "Probar",
"connectionSuccess": "Conexión exitosa.",
"connectionFailed": "Conexión fallida: {message}",
"setDefault": "Establecer como predeterminada",
"defaultLabel": "Predeterminada",
"defaultSet": "Cuenta predeterminada actualizada.",
"removeAccount": "Eliminar",
"removeConfirmTitle": "Eliminar cuenta",
"removeConfirmMessage": "¿Estás seguro de que deseas eliminar la cuenta \"{username}\"?",
"removeConfirm": "Eliminar",
"removeCancel": "Cancelar",
"removeSuccess": "Cuenta eliminada.",
"scopes": "Alcances",
"loadFailed": "Error al cargar configuración: {message}"
},
"ShortcutSettings": { "ShortcutSettings": {
"sectionTitle": "Atajos", "sectionTitle": "Atajos",
"resetDefault": "Restablecer valores predeterminados", "resetDefault": "Restablecer valores predeterminados",

View File

@@ -66,6 +66,7 @@
"mcp": "MCP", "mcp": "MCP",
"skills": "Skills", "skills": "Skills",
"shortcuts": "Raccourcis", "shortcuts": "Raccourcis",
"version_control": "Contrôle de version",
"system": "Système" "system": "Système"
} }
}, },
@@ -128,6 +129,55 @@
"unknown": "La mise à jour a échoué. Veuillez réessayer plus tard." "unknown": "La mise à jour a échoué. Veuillez réessayer plus tard."
} }
}, },
"VersionControlSettings": {
"loading": "Chargement...",
"sectionTitle": "Contrôle de version",
"sectionDescription": "Configurez l'exécutable Git et gérez les comptes GitHub.",
"gitTitle": "Configuration Git",
"gitDescription": "Configurez l'exécutable Git utilisé par l'application.",
"gitDetected": "Git détecté",
"gitNotFound": "Git introuvable sur le système",
"gitVersion": "Version",
"gitPath": "Chemin",
"customGitPath": "Chemin Git personnalisé",
"customGitPathPlaceholder": "/usr/bin/git",
"customGitPathHint": "Laissez vide pour utiliser le chemin détecté automatiquement.",
"test": "Tester",
"testing": "Test en cours...",
"testSuccess": "L'exécutable Git est valide.",
"testFailed": "Test Git échoué : {message}",
"save": "Enregistrer",
"saving": "Enregistrement...",
"saveSuccess": "Paramètres Git enregistrés.",
"saveFailed": "Échec de l'enregistrement : {message}",
"githubTitle": "Comptes GitHub",
"githubDescription": "Gérez les comptes GitHub pour l'authentification. Les jetons sont stockés localement.",
"noAccounts": "Aucun compte GitHub configuré.",
"addAccount": "Ajouter un compte",
"serverUrl": "URL du serveur",
"serverUrlPlaceholder": "https://github.com",
"token": "Jeton d'accès personnel",
"tokenPlaceholder": "ghp_xxxxxxxxxxxx",
"tokenHint": "Générez un jeton dans GitHub → Settings → Developer settings → Personal access tokens.",
"validateAndAdd": "Valider et ajouter",
"validating": "Validation...",
"addSuccess": "Compte {username} ajouté avec succès.",
"addFailed": "Échec de l'ajout du compte : {message}",
"testConnection": "Tester",
"connectionSuccess": "Connexion réussie.",
"connectionFailed": "Échec de la connexion : {message}",
"setDefault": "Définir par défaut",
"defaultLabel": "Par défaut",
"defaultSet": "Compte par défaut mis à jour.",
"removeAccount": "Supprimer",
"removeConfirmTitle": "Supprimer le compte",
"removeConfirmMessage": "Êtes-vous sûr de vouloir supprimer le compte « {username} » ?",
"removeConfirm": "Supprimer",
"removeCancel": "Annuler",
"removeSuccess": "Compte supprimé.",
"scopes": "Portées",
"loadFailed": "Échec du chargement des paramètres : {message}"
},
"ShortcutSettings": { "ShortcutSettings": {
"sectionTitle": "Raccourcis", "sectionTitle": "Raccourcis",
"resetDefault": "Rétablir les valeurs par défaut", "resetDefault": "Rétablir les valeurs par défaut",

View File

@@ -66,6 +66,7 @@
"mcp": "MCP", "mcp": "MCP",
"skills": "Skills", "skills": "Skills",
"shortcuts": "ショートカット", "shortcuts": "ショートカット",
"version_control": "バージョン管理",
"system": "システム" "system": "システム"
} }
}, },
@@ -128,6 +129,55 @@
"unknown": "更新に失敗しました。しばらくしてから再試行してください。" "unknown": "更新に失敗しました。しばらくしてから再試行してください。"
} }
}, },
"VersionControlSettings": {
"loading": "読み込み中...",
"sectionTitle": "バージョン管理",
"sectionDescription": "Git 実行ファイルの設定と GitHub アカウントの管理。",
"gitTitle": "Git 設定",
"gitDescription": "アプリケーションで使用する Git 実行ファイルを設定します。",
"gitDetected": "Git が検出されました",
"gitNotFound": "システムに Git が見つかりません",
"gitVersion": "バージョン",
"gitPath": "パス",
"customGitPath": "カスタム Git パス",
"customGitPathPlaceholder": "/usr/bin/git",
"customGitPathHint": "空欄の場合、自動検出されたパスを使用します。",
"test": "テスト",
"testing": "テスト中...",
"testSuccess": "Git 実行ファイルは有効です。",
"testFailed": "Git テスト失敗:{message}",
"save": "保存",
"saving": "保存中...",
"saveSuccess": "Git 設定を保存しました。",
"saveFailed": "保存に失敗しました:{message}",
"githubTitle": "GitHub アカウント",
"githubDescription": "認証用の GitHub アカウントを管理します。トークンはローカルに保存されます。",
"noAccounts": "GitHub アカウントが設定されていません。",
"addAccount": "アカウントを追加",
"serverUrl": "サーバー URL",
"serverUrlPlaceholder": "https://github.com",
"token": "個人アクセストークン",
"tokenPlaceholder": "ghp_xxxxxxxxxxxx",
"tokenHint": "GitHub → Settings → Developer settings → Personal access tokens でトークンを生成してください。",
"validateAndAdd": "検証して追加",
"validating": "検証中...",
"addSuccess": "アカウント {username} を追加しました。",
"addFailed": "アカウントの追加に失敗しました:{message}",
"testConnection": "テスト",
"connectionSuccess": "接続成功。",
"connectionFailed": "接続失敗:{message}",
"setDefault": "デフォルトに設定",
"defaultLabel": "デフォルト",
"defaultSet": "デフォルトアカウントを更新しました。",
"removeAccount": "削除",
"removeConfirmTitle": "アカウントを削除",
"removeConfirmMessage": "アカウント「{username}」を削除してもよろしいですか?",
"removeConfirm": "削除",
"removeCancel": "キャンセル",
"removeSuccess": "アカウントを削除しました。",
"scopes": "スコープ",
"loadFailed": "設定の読み込みに失敗しました:{message}"
},
"ShortcutSettings": { "ShortcutSettings": {
"sectionTitle": "ショートカット", "sectionTitle": "ショートカット",
"resetDefault": "デフォルトに戻す", "resetDefault": "デフォルトに戻す",

View File

@@ -66,6 +66,7 @@
"mcp": "MCP", "mcp": "MCP",
"skills": "Skills", "skills": "Skills",
"shortcuts": "단축키", "shortcuts": "단축키",
"version_control": "버전 관리",
"system": "시스템" "system": "시스템"
} }
}, },
@@ -128,6 +129,55 @@
"unknown": "업데이트에 실패했습니다. 잠시 후 다시 시도하세요." "unknown": "업데이트에 실패했습니다. 잠시 후 다시 시도하세요."
} }
}, },
"VersionControlSettings": {
"loading": "로딩 중...",
"sectionTitle": "버전 관리",
"sectionDescription": "Git 실행 파일을 설정하고 GitHub 계정을 관리합니다.",
"gitTitle": "Git 설정",
"gitDescription": "애플리케이션에서 사용할 Git 실행 파일을 설정합니다.",
"gitDetected": "Git이 감지되었습니다",
"gitNotFound": "시스템에서 Git을 찾을 수 없습니다",
"gitVersion": "버전",
"gitPath": "경로",
"customGitPath": "사용자 지정 Git 경로",
"customGitPathPlaceholder": "/usr/bin/git",
"customGitPathHint": "비워두면 자동 감지된 경로를 사용합니다.",
"test": "테스트",
"testing": "테스트 중...",
"testSuccess": "Git 실행 파일이 유효합니다.",
"testFailed": "Git 테스트 실패: {message}",
"save": "저장",
"saving": "저장 중...",
"saveSuccess": "Git 설정이 저장되었습니다.",
"saveFailed": "저장 실패: {message}",
"githubTitle": "GitHub 계정",
"githubDescription": "인증용 GitHub 계정을 관리합니다. 토큰은 로컬에 저장됩니다.",
"noAccounts": "설정된 GitHub 계정이 없습니다.",
"addAccount": "계정 추가",
"serverUrl": "서버 URL",
"serverUrlPlaceholder": "https://github.com",
"token": "개인 액세스 토큰",
"tokenPlaceholder": "ghp_xxxxxxxxxxxx",
"tokenHint": "GitHub → Settings → Developer settings → Personal access tokens에서 토큰을 생성하세요.",
"validateAndAdd": "검증 및 추가",
"validating": "검증 중...",
"addSuccess": "계정 {username}이(가) 추가되었습니다.",
"addFailed": "계정 추가 실패: {message}",
"testConnection": "테스트",
"connectionSuccess": "연결 성공.",
"connectionFailed": "연결 실패: {message}",
"setDefault": "기본값으로 설정",
"defaultLabel": "기본",
"defaultSet": "기본 계정이 업데이트되었습니다.",
"removeAccount": "삭제",
"removeConfirmTitle": "계정 삭제",
"removeConfirmMessage": "계정 \"{username}\"을(를) 삭제하시겠습니까?",
"removeConfirm": "삭제",
"removeCancel": "취소",
"removeSuccess": "계정이 삭제되었습니다.",
"scopes": "범위",
"loadFailed": "설정 로드 실패: {message}"
},
"ShortcutSettings": { "ShortcutSettings": {
"sectionTitle": "단축키", "sectionTitle": "단축키",
"resetDefault": "기본값으로 재설정", "resetDefault": "기본값으로 재설정",

View File

@@ -66,6 +66,7 @@
"mcp": "MCP", "mcp": "MCP",
"skills": "Skills", "skills": "Skills",
"shortcuts": "Atalhos", "shortcuts": "Atalhos",
"version_control": "Controle de versão",
"system": "Sistema" "system": "Sistema"
} }
}, },
@@ -128,6 +129,55 @@
"unknown": "Falha na atualização. Tente novamente mais tarde." "unknown": "Falha na atualização. Tente novamente mais tarde."
} }
}, },
"VersionControlSettings": {
"loading": "Carregando...",
"sectionTitle": "Controle de versão",
"sectionDescription": "Configure o executável Git e gerencie contas do GitHub.",
"gitTitle": "Configuração do Git",
"gitDescription": "Configure o executável Git usado pelo aplicativo.",
"gitDetected": "Git detectado",
"gitNotFound": "Git não encontrado no sistema",
"gitVersion": "Versão",
"gitPath": "Caminho",
"customGitPath": "Caminho personalizado do Git",
"customGitPathPlaceholder": "/usr/bin/git",
"customGitPathHint": "Deixe vazio para usar o caminho detectado automaticamente.",
"test": "Testar",
"testing": "Testando...",
"testSuccess": "Executável Git é válido.",
"testFailed": "Teste do Git falhou: {message}",
"save": "Salvar",
"saving": "Salvando...",
"saveSuccess": "Configurações do Git salvas.",
"saveFailed": "Falha ao salvar: {message}",
"githubTitle": "Contas do GitHub",
"githubDescription": "Gerencie contas do GitHub para autenticação. Os tokens são armazenados localmente.",
"noAccounts": "Nenhuma conta do GitHub configurada.",
"addAccount": "Adicionar conta",
"serverUrl": "URL do servidor",
"serverUrlPlaceholder": "https://github.com",
"token": "Token de acesso pessoal",
"tokenPlaceholder": "ghp_xxxxxxxxxxxx",
"tokenHint": "Gere um token em GitHub → Settings → Developer settings → Personal access tokens.",
"validateAndAdd": "Validar e adicionar",
"validating": "Validando...",
"addSuccess": "Conta {username} adicionada com sucesso.",
"addFailed": "Falha ao adicionar conta: {message}",
"testConnection": "Testar",
"connectionSuccess": "Conexão bem-sucedida.",
"connectionFailed": "Falha na conexão: {message}",
"setDefault": "Definir como padrão",
"defaultLabel": "Padrão",
"defaultSet": "Conta padrão atualizada.",
"removeAccount": "Remover",
"removeConfirmTitle": "Remover conta",
"removeConfirmMessage": "Tem certeza de que deseja remover a conta \"{username}\"?",
"removeConfirm": "Remover",
"removeCancel": "Cancelar",
"removeSuccess": "Conta removida.",
"scopes": "Escopos",
"loadFailed": "Falha ao carregar configurações: {message}"
},
"ShortcutSettings": { "ShortcutSettings": {
"sectionTitle": "Atalhos", "sectionTitle": "Atalhos",
"resetDefault": "Restaurar padrões", "resetDefault": "Restaurar padrões",

View File

@@ -66,6 +66,7 @@
"mcp": "MCP", "mcp": "MCP",
"skills": "Skills", "skills": "Skills",
"shortcuts": "快捷键", "shortcuts": "快捷键",
"version_control": "版本控制",
"system": "系统" "system": "系统"
} }
}, },
@@ -128,6 +129,55 @@
"unknown": "更新失败,请稍后重试。" "unknown": "更新失败,请稍后重试。"
} }
}, },
"VersionControlSettings": {
"loading": "加载中...",
"sectionTitle": "版本控制",
"sectionDescription": "配置 Git 可执行文件并管理 GitHub 账号。",
"gitTitle": "Git 配置",
"gitDescription": "配置应用使用的 Git 可执行文件。",
"gitDetected": "已检测到 Git",
"gitNotFound": "未在系统中找到 Git",
"gitVersion": "版本",
"gitPath": "路径",
"customGitPath": "自定义 Git 路径",
"customGitPathPlaceholder": "/usr/bin/git",
"customGitPathHint": "留空则使用自动检测的路径。",
"test": "测试",
"testing": "测试中...",
"testSuccess": "Git 可执行文件有效。",
"testFailed": "Git 测试失败:{message}",
"save": "保存",
"saving": "保存中...",
"saveSuccess": "Git 设置已保存。",
"saveFailed": "保存失败:{message}",
"githubTitle": "GitHub 账号",
"githubDescription": "管理用于身份验证的 GitHub 账号。令牌存储在本地。",
"noAccounts": "暂无 GitHub 账号。",
"addAccount": "添加账号",
"serverUrl": "服务器地址",
"serverUrlPlaceholder": "https://github.com",
"token": "个人访问令牌",
"tokenPlaceholder": "ghp_xxxxxxxxxxxx",
"tokenHint": "在 GitHub → Settings → Developer settings → Personal access tokens 中生成令牌。",
"validateAndAdd": "验证并添加",
"validating": "验证中...",
"addSuccess": "账号 {username} 添加成功。",
"addFailed": "添加账号失败:{message}",
"testConnection": "测试",
"connectionSuccess": "连接成功。",
"connectionFailed": "连接失败:{message}",
"setDefault": "设为默认",
"defaultLabel": "默认",
"defaultSet": "默认账号已更新。",
"removeAccount": "删除",
"removeConfirmTitle": "删除账号",
"removeConfirmMessage": "确定要删除账号「{username}」吗?",
"removeConfirm": "删除",
"removeCancel": "取消",
"removeSuccess": "账号已删除。",
"scopes": "权限范围",
"loadFailed": "加载设置失败:{message}"
},
"ShortcutSettings": { "ShortcutSettings": {
"sectionTitle": "快捷键", "sectionTitle": "快捷键",
"resetDefault": "恢复默认", "resetDefault": "恢复默认",

View File

@@ -66,6 +66,7 @@
"mcp": "MCP", "mcp": "MCP",
"skills": "Skills", "skills": "Skills",
"shortcuts": "快捷鍵", "shortcuts": "快捷鍵",
"version_control": "版本控制",
"system": "系統" "system": "系統"
} }
}, },
@@ -128,6 +129,55 @@
"unknown": "更新失敗,請稍後再試。" "unknown": "更新失敗,請稍後再試。"
} }
}, },
"VersionControlSettings": {
"loading": "載入中...",
"sectionTitle": "版本控制",
"sectionDescription": "設定 Git 執行檔並管理 GitHub 帳號。",
"gitTitle": "Git 設定",
"gitDescription": "設定應用程式使用的 Git 執行檔。",
"gitDetected": "已偵測到 Git",
"gitNotFound": "未在系統中找到 Git",
"gitVersion": "版本",
"gitPath": "路徑",
"customGitPath": "自訂 Git 路徑",
"customGitPathPlaceholder": "/usr/bin/git",
"customGitPathHint": "留空則使用自動偵測的路徑。",
"test": "測試",
"testing": "測試中...",
"testSuccess": "Git 執行檔有效。",
"testFailed": "Git 測試失敗:{message}",
"save": "儲存",
"saving": "儲存中...",
"saveSuccess": "Git 設定已儲存。",
"saveFailed": "儲存失敗:{message}",
"githubTitle": "GitHub 帳號",
"githubDescription": "管理用於身份驗證的 GitHub 帳號。權杖儲存在本機。",
"noAccounts": "尚未設定 GitHub 帳號。",
"addAccount": "新增帳號",
"serverUrl": "伺服器網址",
"serverUrlPlaceholder": "https://github.com",
"token": "個人存取權杖",
"tokenPlaceholder": "ghp_xxxxxxxxxxxx",
"tokenHint": "在 GitHub → Settings → Developer settings → Personal access tokens 中產生權杖。",
"validateAndAdd": "驗證並新增",
"validating": "驗證中...",
"addSuccess": "帳號 {username} 新增成功。",
"addFailed": "新增帳號失敗:{message}",
"testConnection": "測試",
"connectionSuccess": "連線成功。",
"connectionFailed": "連線失敗:{message}",
"setDefault": "設為預設",
"defaultLabel": "預設",
"defaultSet": "預設帳號已更新。",
"removeAccount": "刪除",
"removeConfirmTitle": "刪除帳號",
"removeConfirmMessage": "確定要刪除帳號「{username}」嗎?",
"removeConfirm": "刪除",
"removeCancel": "取消",
"removeSuccess": "帳號已刪除。",
"scopes": "權限範圍",
"loadFailed": "載入設定失敗:{message}"
},
"ShortcutSettings": { "ShortcutSettings": {
"sectionTitle": "快捷鍵", "sectionTitle": "快捷鍵",
"resetDefault": "恢復預設", "resetDefault": "恢復預設",

View File

@@ -41,6 +41,10 @@ import type {
GitLogEntry, GitLogEntry,
SystemLanguageSettings, SystemLanguageSettings,
SystemProxySettings, SystemProxySettings,
GitDetectResult,
GitSettings,
GitHubAccountsSettings,
GitHubTokenValidation,
McpAppType, McpAppType,
LocalMcpServer, LocalMcpServer,
McpMarketplaceProvider, McpMarketplaceProvider,
@@ -302,6 +306,43 @@ export async function updateSystemLanguageSettings(
return invoke("update_system_language_settings", { settings }) return invoke("update_system_language_settings", { settings })
} }
// --- Version Control ---
export async function detectGit(): Promise<GitDetectResult> {
return invoke("detect_git")
}
export async function testGitPath(path: string): Promise<GitDetectResult> {
return invoke("test_git_path", { path })
}
export async function getGitSettings(): Promise<GitSettings> {
return invoke("get_git_settings")
}
export async function updateGitSettings(
settings: GitSettings
): Promise<GitSettings> {
return invoke("update_git_settings", { settings })
}
export async function getGitHubAccounts(): Promise<GitHubAccountsSettings> {
return invoke("get_github_accounts")
}
export async function validateGitHubToken(
serverUrl: string,
token: string
): Promise<GitHubTokenValidation> {
return invoke("validate_github_token", { serverUrl, token })
}
export async function updateGitHubAccounts(
settings: GitHubAccountsSettings
): Promise<GitHubAccountsSettings> {
return invoke("update_github_accounts", { settings })
}
export async function mcpScanLocal(): Promise<LocalMcpServer[]> { export async function mcpScanLocal(): Promise<LocalMcpServer[]> {
return invoke("mcp_scan_local") return invoke("mcp_scan_local")
} }

View File

@@ -523,6 +523,41 @@ export interface SystemLanguageSettings {
language: AppLocale language: AppLocale
} }
// --- Version Control ---
export interface GitDetectResult {
installed: boolean
version: string | null
path: string | null
}
export interface GitSettings {
custom_path: string | null
}
export interface GitHubAccount {
id: string
server_url: string
username: string
token: string
scopes: string[]
avatar_url: string | null
is_default: boolean
created_at: string
}
export interface GitHubAccountsSettings {
accounts: GitHubAccount[]
}
export interface GitHubTokenValidation {
success: boolean
username: string | null
scopes: string[]
avatar_url: string | null
message: string | null
}
export type McpAppType = "claude_code" | "codex" | "open_code" export type McpAppType = "claude_code" | "codex" | "open_code"
export interface LocalMcpServer { export interface LocalMcpServer {