* feat(providers): add notes field for provider management - Add notes field to Provider model (backend and frontend) - Display notes with higher priority than URL in provider card - Style notes as non-clickable text to differentiate from URLs - Add notes input field in provider form - Add i18n support (zh/en) for notes field * chore: format code and clean up unused props - Run cargo fmt on Rust backend code - Format TypeScript imports and code style - Remove unused appId prop from ProviderPresetSelector - Clean up unused variables in tests - Integrate notes field handling in provider dialogs * feat(deeplink): implement ccswitch:// protocol for provider import Add deep link support to enable one-click provider configuration import via ccswitch:// URLs. Backend: - Implement URL parsing and validation (src-tauri/src/deeplink.rs) - Add Tauri commands for parse and import (src-tauri/src/commands/deeplink.rs) - Register ccswitch:// protocol in macOS Info.plist - Add comprehensive unit tests (src-tauri/tests/deeplink_import.rs) Frontend: - Create confirmation dialog with security review UI (src/components/DeepLinkImportDialog.tsx) - Add API wrapper (src/lib/api/deeplink.ts) - Integrate event listeners in App.tsx Configuration: - Update Tauri config for deep link handling - Add i18n support for Chinese and English - Include test page for deep link validation (deeplink-test.html) Files: 15 changed, 1312 insertions(+) * chore(deeplink): integrate deep link handling into app lifecycle Wire up deep link infrastructure with app initialization and event handling. Backend Integration: - Register deep link module and commands in mod.rs - Add URL handling in app setup (src-tauri/src/lib.rs:handle_deeplink_url) - Handle deep links from single instance callback (Windows/Linux CLI) - Handle deep links from macOS system events - Add tauri-plugin-deep-link dependency (Cargo.toml) Frontend Integration: - Listen for deeplink-import/deeplink-error events in App.tsx - Update DeepLinkImportDialog component imports Configuration: - Enable deep link plugin in tauri.conf.json - Update Cargo.lock for new dependencies Localization: - Add Chinese translations for deep link UI (zh.json) - Add English translations for deep link UI (en.json) Files: 9 changed, 359 insertions(+), 18 deletions(-) * refactor(deeplink): enhance Codex provider template generation Align deep link import with UI preset generation logic by: - Adding complete config.toml template matching frontend defaults - Generating safe provider name from sanitized input - Including model_provider, reasoning_effort, and wire_api settings - Removing minimal template that only contained base_url - Cleaning up deprecated test file deeplink-test.html * style: fix clippy uninlined_format_args warnings Apply clippy --fix to use inline format arguments in: - src/mcp.rs (8 fixes) - src/services/env_manager.rs (10 fixes) * style: apply code formatting and cleanup - Format TypeScript files with Prettier (App.tsx, EnvWarningBanner.tsx, formatters.ts) - Organize Rust imports and module order alphabetically - Add newline at end of JSON files (en.json, zh.json) - Update Cargo.lock for dependency changes * feat: add model name configuration support for Codex and fix Gemini model handling - Add visual model name input field for Codex providers - Add model name extraction and update utilities in providerConfigUtils - Implement model name state management in useCodexConfigState hook - Add conditional model field rendering in CodexFormFields (non-official only) - Integrate model name sync with TOML config in ProviderForm - Fix Gemini deeplink model injection bug - Correct environment variable name from GOOGLE_GEMINI_MODEL to GEMINI_MODEL - Add test cases for Gemini model injection (with/without model) - All tests passing (9/9) - Fix Gemini model field binding in edit mode - Add geminiModel state to useGeminiConfigState hook - Extract model value during initialization and reset - Sync model field with geminiEnv state to prevent data loss on submit - Fix missing model value display when editing Gemini providers Changes: - 6 files changed, 245 insertions(+), 13 deletions(-)
205 lines
6.4 KiB
TypeScript
205 lines
6.4 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { listen } from "@tauri-apps/api/event";
|
|
import { DeepLinkImportRequest, deeplinkApi } from "@/lib/api/deeplink";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { toast } from "sonner";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
|
|
interface DeeplinkError {
|
|
url: string;
|
|
error: string;
|
|
}
|
|
|
|
export function DeepLinkImportDialog() {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
const [request, setRequest] = useState<DeepLinkImportRequest | null>(null);
|
|
const [isImporting, setIsImporting] = useState(false);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Listen for deep link import events
|
|
const unlistenImport = listen<DeepLinkImportRequest>(
|
|
"deeplink-import",
|
|
(event) => {
|
|
console.log("Deep link import event received:", event.payload);
|
|
setRequest(event.payload);
|
|
setIsOpen(true);
|
|
},
|
|
);
|
|
|
|
// Listen for deep link error events
|
|
const unlistenError = listen<DeeplinkError>("deeplink-error", (event) => {
|
|
console.error("Deep link error:", event.payload);
|
|
toast.error(t("deeplink.parseError"), {
|
|
description: event.payload.error,
|
|
});
|
|
});
|
|
|
|
return () => {
|
|
unlistenImport.then((fn) => fn());
|
|
unlistenError.then((fn) => fn());
|
|
};
|
|
}, [t]);
|
|
|
|
const handleImport = async () => {
|
|
if (!request) return;
|
|
|
|
setIsImporting(true);
|
|
|
|
try {
|
|
await deeplinkApi.importFromDeeplink(request);
|
|
|
|
// Invalidate provider queries to refresh the list
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["providers", request.app],
|
|
});
|
|
|
|
toast.success(t("deeplink.importSuccess"), {
|
|
description: t("deeplink.importSuccessDescription", {
|
|
name: request.name,
|
|
}),
|
|
});
|
|
|
|
setIsOpen(false);
|
|
setRequest(null);
|
|
} catch (error) {
|
|
console.error("Failed to import provider from deep link:", error);
|
|
toast.error(t("deeplink.importError"), {
|
|
description: error instanceof Error ? error.message : String(error),
|
|
});
|
|
} finally {
|
|
setIsImporting(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setIsOpen(false);
|
|
setRequest(null);
|
|
};
|
|
|
|
if (!request) return null;
|
|
|
|
// Mask API key for display (show first 4 chars + ***)
|
|
const maskedApiKey =
|
|
request.apiKey.length > 4
|
|
? `${request.apiKey.substring(0, 4)}${"*".repeat(20)}`
|
|
: "****";
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<DialogContent className="sm:max-w-[500px]">
|
|
{/* 标题显式左对齐,避免默认居中样式影响 */}
|
|
<DialogHeader className="text-left sm:text-left">
|
|
<DialogTitle>{t("deeplink.confirmImport")}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("deeplink.confirmImportDescription")}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{/* 主体内容整体右移,略大于标题内边距,让内容看起来不贴边 */}
|
|
<div className="space-y-4 px-8 py-4">
|
|
{/* App Type */}
|
|
<div className="grid grid-cols-3 items-center gap-4">
|
|
<div className="font-medium text-sm text-muted-foreground">
|
|
{t("deeplink.app")}
|
|
</div>
|
|
<div className="col-span-2 text-sm font-medium capitalize">
|
|
{request.app}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Provider Name */}
|
|
<div className="grid grid-cols-3 items-center gap-4">
|
|
<div className="font-medium text-sm text-muted-foreground">
|
|
{t("deeplink.providerName")}
|
|
</div>
|
|
<div className="col-span-2 text-sm font-medium">{request.name}</div>
|
|
</div>
|
|
|
|
{/* Homepage */}
|
|
<div className="grid grid-cols-3 items-center gap-4">
|
|
<div className="font-medium text-sm text-muted-foreground">
|
|
{t("deeplink.homepage")}
|
|
</div>
|
|
<div className="col-span-2 text-sm break-all text-blue-600 dark:text-blue-400">
|
|
{request.homepage}
|
|
</div>
|
|
</div>
|
|
|
|
{/* API Endpoint */}
|
|
<div className="grid grid-cols-3 items-center gap-4">
|
|
<div className="font-medium text-sm text-muted-foreground">
|
|
{t("deeplink.endpoint")}
|
|
</div>
|
|
<div className="col-span-2 text-sm break-all">
|
|
{request.endpoint}
|
|
</div>
|
|
</div>
|
|
|
|
{/* API Key (masked) */}
|
|
<div className="grid grid-cols-3 items-center gap-4">
|
|
<div className="font-medium text-sm text-muted-foreground">
|
|
{t("deeplink.apiKey")}
|
|
</div>
|
|
<div className="col-span-2 text-sm font-mono text-muted-foreground">
|
|
{maskedApiKey}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Model (if present) */}
|
|
{request.model && (
|
|
<div className="grid grid-cols-3 items-center gap-4">
|
|
<div className="font-medium text-sm text-muted-foreground">
|
|
{t("deeplink.model")}
|
|
</div>
|
|
<div className="col-span-2 text-sm font-mono">
|
|
{request.model}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Notes (if present) */}
|
|
{request.notes && (
|
|
<div className="grid grid-cols-3 items-start gap-4">
|
|
<div className="font-medium text-sm text-muted-foreground">
|
|
{t("deeplink.notes")}
|
|
</div>
|
|
<div className="col-span-2 text-sm text-muted-foreground">
|
|
{request.notes}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Warning */}
|
|
<div className="rounded-lg bg-yellow-50 dark:bg-yellow-900/20 p-3 text-sm text-yellow-800 dark:text-yellow-200">
|
|
{t("deeplink.warning")}
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleCancel}
|
|
disabled={isImporting}
|
|
>
|
|
{t("common.cancel")}
|
|
</Button>
|
|
<Button onClick={handleImport} disabled={isImporting}>
|
|
{isImporting ? t("deeplink.importing") : t("deeplink.import")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|