feat: v0.2.0 - add system tray, clean up unused role field
- Replace text '+' icon with centered SVG plus icon for the add-provider button - Add system tray: minimize-to-tray and quit-from-tray menu (minimize hides window, close exits app) - Remove unused model role field (no functional effect, only stored/displayed) - Fix multiple providers showing as active in Kimi Code mode: only the default_model's provider is marked active - Copy /reload to clipboard after provider switch as a convenience hint - Bump version to 0.2.0
This commit is contained in:
1 parent
0ce89b09d9
commit
948e96352e
30 files changed
+543
-255
No files matched your search
+21
-10
@@ -8,7 +8,7 @@ import { useTranslation } from "./i18n";
|
||||
import { getDefaultMaxContextSize } from "./lib/model-defaults";
|
||||
import type { Agent, Model, Provider } from "./types";
|
||||
|
||||
const AGENT_STORAGE_KEY = "pi-switch-agent";
|
||||
const AGENT_STORAGE_KEY = "kimi-switch-agent";
|
||||
|
||||
function getProviderDefaultModel(provider: Provider): string | null {
|
||||
const raw = provider.raw_other as Record<string, unknown> | undefined;
|
||||
@@ -65,6 +65,7 @@ export default function App() {
|
||||
const [view, setView] = useState<"list" | "edit">("list");
|
||||
const [editingProvider, setEditingProvider] = useState<string>("");
|
||||
const [loadTimeout, setLoadTimeout] = useState(false);
|
||||
const [switchMessage, setSwitchMessage] = useState<string | null>(null);
|
||||
|
||||
const setAgent = (next: Agent) => {
|
||||
setAgentState(next);
|
||||
@@ -243,7 +244,7 @@ export default function App() {
|
||||
|
||||
// Only the selected provider is active. All other providers (including
|
||||
// Kimi native/managed and custom) are deactivated so the target agent
|
||||
// follows Pi Switch's choice exactly.
|
||||
// follows Kimi Switch's choice exactly.
|
||||
const providers: Record<string, Provider> = {};
|
||||
for (const [key, p] of Object.entries(cfg.providers)) {
|
||||
providers[key] = { ...p, active: key === name };
|
||||
@@ -271,9 +272,19 @@ export default function App() {
|
||||
return { ...cfg, providers, default_model };
|
||||
});
|
||||
|
||||
// Persist the full config to Pi Switch's SQLite and activate the selected provider.
|
||||
// Persist the full config to Kimi Switch's SQLite and activate the selected provider.
|
||||
await save();
|
||||
await invoke("activate_agent_config_command", { agent });
|
||||
|
||||
// /reload is an interactive Kimi Code TUI command with no CLI equivalent,
|
||||
// so we copy it to the clipboard as a convenience.
|
||||
try {
|
||||
await navigator.clipboard.writeText("/reload");
|
||||
setSwitchMessage(t("reloadCopiedHint"));
|
||||
setTimeout(() => setSwitchMessage(null), 4000);
|
||||
} catch {
|
||||
// Clipboard may be unavailable; ignore silently.
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyProviderJson = (provider: Provider, models: Model[]) => {
|
||||
@@ -391,6 +402,12 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{switchMessage && (
|
||||
<div className="bg-green-900/20 text-green-400 p-2 text-sm border-b border-green-500/20 text-center">
|
||||
{switchMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<main className="flex-1 overflow-hidden relative">
|
||||
{view === "list" ? (
|
||||
<ProviderList
|
||||
@@ -452,11 +469,6 @@ export default function App() {
|
||||
});
|
||||
}}
|
||||
onModelAdd={() => {
|
||||
const providerModels = Object.values(config.models).filter(
|
||||
(m) => m.provider === currentProvider.name
|
||||
);
|
||||
const roles = ["Sonnet", "Opus", "Fable", "Haiku"];
|
||||
const defaultRole = roles[providerModels.length % roles.length];
|
||||
const alias = `新模型[${currentProvider.name}]`;
|
||||
updateConfig((cfg) => ({
|
||||
...cfg,
|
||||
@@ -468,9 +480,8 @@ export default function App() {
|
||||
model: "",
|
||||
max_context_size: getDefaultMaxContextSize(alias),
|
||||
display_name: null,
|
||||
role: defaultRole,
|
||||
supports_1m: false,
|
||||
capabilities: [],
|
||||
capabilities: agent === "kimi_code" ? ["thinking"] : [],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useTranslation } from "../i18n";
|
||||
import type { TranslationKey } from "../i18n/zh";
|
||||
import { getDefaultMaxContextSize } from "../lib/model-defaults";
|
||||
import { AgentSettingsPanel } from "./AgentSettingsPanel";
|
||||
import type { Agent, DiscoveredModel, Model, Provider, ProviderType } from "../types";
|
||||
|
||||
const KNOWN_CAPABILITIES = [
|
||||
"thinking",
|
||||
"always_thinking",
|
||||
"image_in",
|
||||
"video_in",
|
||||
"tool_use",
|
||||
] as const;
|
||||
|
||||
const CAPABILITY_LABELS: Record<
|
||||
(typeof KNOWN_CAPABILITIES)[number],
|
||||
TranslationKey
|
||||
> = {
|
||||
thinking: "capThinking",
|
||||
always_thinking: "capAlwaysThinking",
|
||||
image_in: "capImageIn",
|
||||
video_in: "capVideoIn",
|
||||
tool_use: "capToolUse",
|
||||
};
|
||||
|
||||
const PROVIDER_TYPES: ProviderType[] = [
|
||||
"openai",
|
||||
"openai_responses",
|
||||
@@ -346,6 +366,7 @@ function ModelMapping({
|
||||
const [discovered, setDiscovered] = useState<DiscoveredModel[] | null>(null);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [discoverError, setDiscoverError] = useState<string | null>(null);
|
||||
const [fetchThinking, setFetchThinking] = useState(true);
|
||||
|
||||
const handleDiscover = async () => {
|
||||
setDiscovering(true);
|
||||
@@ -379,7 +400,6 @@ function ModelMapping({
|
||||
for (const dm of discovered) {
|
||||
if (!selected.has(dm.id)) continue;
|
||||
const alias = dm.id.replace(/[^a-zA-Z0-9_-]/g, "-");
|
||||
const role = `${dm.id}[${provider.name}]`;
|
||||
const max_context_size = dm.max_context_size ?? getDefaultMaxContextSize(dm.id);
|
||||
toAdd.push({
|
||||
alias,
|
||||
@@ -387,9 +407,8 @@ function ModelMapping({
|
||||
model: dm.id,
|
||||
max_context_size,
|
||||
display_name: dm.display_name,
|
||||
role,
|
||||
supports_1m: max_context_size >= 1_000_000,
|
||||
capabilities: [],
|
||||
capabilities: fetchThinking ? ["thinking"] : [],
|
||||
});
|
||||
}
|
||||
onBulkAdd(toAdd);
|
||||
@@ -434,6 +453,15 @@ function ModelMapping({
|
||||
<div className="text-sm font-medium">
|
||||
{t("discoveredModels", { count: discovered.length })}
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm text-[#e5e5e7] cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={fetchThinking}
|
||||
onChange={(e) => setFetchThinking(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-[#2a2a2e] bg-[#1f1f23] text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{t("fetchEnableThinking")}
|
||||
</label>
|
||||
<div className="max-h-48 overflow-auto space-y-1">
|
||||
{discovered.map((m) => (
|
||||
<label
|
||||
@@ -467,7 +495,6 @@ function ModelMapping({
|
||||
<table className="w-full min-w-[640px] text-sm">
|
||||
<thead className="bg-[#1f1f23] text-gray-400">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 font-medium">{t("modelRole")}</th>
|
||||
<th className="text-left px-4 py-3 font-medium">{t("displayName")}</th>
|
||||
<th className="text-left px-4 py-3 font-medium">{t("actualModel")}</th>
|
||||
<th className="text-left px-4 py-3 font-medium w-28">{t("contextSize")}</th>
|
||||
@@ -482,18 +509,6 @@ function ModelMapping({
|
||||
<tbody className="divide-y divide-[#2a2a2e]">
|
||||
{models.map((m) => (
|
||||
<tr key={m.alias} className="hover:bg-[#1c1c20]">
|
||||
<td className="px-4 py-2">
|
||||
<select
|
||||
className="w-full bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
value={m.role || "Sonnet"}
|
||||
onChange={(e) => onModelChange({ ...m, role: e.target.value })}
|
||||
>
|
||||
<option value="Sonnet">Sonnet</option>
|
||||
<option value="Opus">Opus</option>
|
||||
<option value="Fable">Fable</option>
|
||||
<option value="Haiku">Haiku</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<span className="text-sm text-gray-400">
|
||||
{m.model ? `${m.model}[${m.provider}]` : m.alias}
|
||||
@@ -539,18 +554,10 @@ function ModelMapping({
|
||||
</td>
|
||||
{agent === "kimi_code" && (
|
||||
<td className="px-4 py-2">
|
||||
<input
|
||||
className="w-full bg-transparent border border-[#2a2a2e] rounded px-2 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
value={(m.capabilities || []).join(", ")}
|
||||
placeholder="thinking, image_in"
|
||||
onChange={(e) =>
|
||||
onModelChange({
|
||||
...m,
|
||||
capabilities: e.target.value
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
})
|
||||
<CapabilitiesCell
|
||||
capabilities={m.capabilities || []}
|
||||
onChange={(next) =>
|
||||
onModelChange({ ...m, capabilities: next })
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
@@ -668,3 +675,55 @@ function JsonPreview({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CapabilitiesCell({
|
||||
capabilities,
|
||||
onChange,
|
||||
}: {
|
||||
capabilities: string[];
|
||||
onChange: (next: string[]) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const known = new Set<string>(KNOWN_CAPABILITIES);
|
||||
const customs = capabilities.filter((c) => !known.has(c));
|
||||
|
||||
const toggle = (cap: string, on: boolean) => {
|
||||
if (on) {
|
||||
onChange([...capabilities.filter((c) => c !== cap), cap]);
|
||||
} else {
|
||||
onChange(capabilities.filter((c) => c !== cap));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
|
||||
{KNOWN_CAPABILITIES.map((cap) => (
|
||||
<label
|
||||
key={cap}
|
||||
className="flex items-center gap-1 text-xs text-gray-400 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={capabilities.includes(cap)}
|
||||
onChange={(e) => toggle(cap, e.target.checked)}
|
||||
className="w-3.5 h-3.5 rounded border-[#2a2a2e] bg-[#1f1f23] text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{t(CAPABILITY_LABELS[cap])}
|
||||
</label>
|
||||
))}
|
||||
<input
|
||||
className="w-28 bg-transparent border border-[#2a2a2e] rounded px-1.5 py-1 text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
value={customs.join(", ")}
|
||||
placeholder={t("customCapabilities")}
|
||||
onChange={(e) => {
|
||||
const newCustoms = e.target.value
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const kept = capabilities.filter((c) => known.has(c));
|
||||
onChange([...kept, ...newCustoms]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -66,9 +66,21 @@ export function ProviderList({
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
title={t("addProvider")}
|
||||
className="w-9 h-9 flex items-center justify-center rounded-full bg-orange-500 hover:bg-orange-600 text-white text-xl shadow-lg focus:ring-2 focus:ring-orange-400 focus:outline-none transition-colors"
|
||||
className="w-9 h-9 flex items-center justify-center rounded-full bg-orange-500 hover:bg-orange-600 text-white shadow-lg focus:ring-2 focus:ring-orange-400 focus:outline-none transition-colors"
|
||||
>
|
||||
+
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -80,9 +92,21 @@ export function ProviderList({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
className="mt-4 w-12 h-12 rounded-full bg-orange-500 hover:bg-orange-600 text-white text-2xl shadow-lg flex items-center justify-center mx-auto"
|
||||
className="mt-4 w-12 h-12 rounded-full bg-orange-500 hover:bg-orange-600 text-white shadow-lg flex items-center justify-center mx-auto"
|
||||
>
|
||||
+
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
+9
-2
@@ -2,7 +2,7 @@ import type { TranslationKey } from "./zh";
|
||||
|
||||
export const enTranslations: Record<TranslationKey, string> = {
|
||||
// App / global
|
||||
appTitle: "Pi Switch",
|
||||
appTitle: "Kimi Switch",
|
||||
loading: "Loading...",
|
||||
loadTimeout: "Loading is taking a while. Try restarting the app if it stays stuck.",
|
||||
loadFailed: "Failed to load:",
|
||||
@@ -58,7 +58,6 @@ export const enTranslations: Record<TranslationKey, string> = {
|
||||
fetchingModels: "Fetching...",
|
||||
discoveredModels: "Discovered {count} models",
|
||||
addSelected: "Add selected",
|
||||
modelRole: "Model role",
|
||||
displayName: "Display name",
|
||||
actualModel: "Actual model",
|
||||
contextSize: "Context size",
|
||||
@@ -88,6 +87,7 @@ export const enTranslations: Record<TranslationKey, string> = {
|
||||
switchTo: "Switch to",
|
||||
inUse: "In use",
|
||||
switchReloadHint: "After switching providers, run /reload in Kimi Code to apply the change.",
|
||||
reloadCopiedHint: "Provider switched. /reload copied to clipboard; paste it in Kimi Code.",
|
||||
unsavedChanges: "Unsaved changes",
|
||||
qwenActivateNotImplemented: "Qwen Code activation will be implemented in a follow-up.",
|
||||
agentSettings: "Global Settings",
|
||||
@@ -123,6 +123,13 @@ export const enTranslations: Record<TranslationKey, string> = {
|
||||
importConfigConfirm: "Importing will overwrite unsaved changes. Continue?",
|
||||
importConfigFailed: "Import failed: {message}",
|
||||
capabilities: "Capabilities",
|
||||
fetchEnableThinking: "Enable thinking for fetched models",
|
||||
capThinking: "Thinking",
|
||||
capAlwaysThinking: "Always Thinking",
|
||||
capImageIn: "Image Input",
|
||||
capVideoIn: "Video Input",
|
||||
capToolUse: "Tool Use",
|
||||
customCapabilities: "Other capabilities (comma-separated)",
|
||||
|
||||
// Languages
|
||||
language: "Language",
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ interface I18nContextValue {
|
||||
|
||||
const I18nContext = createContext<I18nContextValue | null>(null);
|
||||
|
||||
const STORAGE_KEY = "pi-switch-lang";
|
||||
const STORAGE_KEY = "kimi-switch-lang";
|
||||
|
||||
function getInitialLang(): Language {
|
||||
try {
|
||||
|
||||
+9
-2
@@ -1,6 +1,6 @@
|
||||
export const zhTranslations = {
|
||||
// App / global
|
||||
appTitle: "Pi Switch",
|
||||
appTitle: "Kimi Switch",
|
||||
loading: "加载中...",
|
||||
loadTimeout: "加载时间较长,如果一直卡住,请尝试重新启动应用。",
|
||||
loadFailed: "加载失败:",
|
||||
@@ -56,7 +56,6 @@ export const zhTranslations = {
|
||||
fetchingModels: "获取中...",
|
||||
discoveredModels: "发现 {count} 个模型",
|
||||
addSelected: "添加选中的",
|
||||
modelRole: "模型角色",
|
||||
displayName: "显示名称",
|
||||
actualModel: "实际请求模型",
|
||||
contextSize: "上下文长度",
|
||||
@@ -86,6 +85,7 @@ export const zhTranslations = {
|
||||
switchTo: "切换使用",
|
||||
inUse: "使用中",
|
||||
switchReloadHint: "切换供应商后,请在 Kimi Code 中执行 /reload 以生效。",
|
||||
reloadCopiedHint: "已切换供应商,/reload 已复制到剪贴板,请在 Kimi Code 中粘贴执行。",
|
||||
unsavedChanges: "未保存修改",
|
||||
qwenActivateNotImplemented: "Qwen Code 启用功能将在后续实现。",
|
||||
agentSettings: "全局配置",
|
||||
@@ -121,6 +121,13 @@ export const zhTranslations = {
|
||||
importConfigConfirm: "导入会覆盖当前未保存的修改,是否继续?",
|
||||
importConfigFailed: "导入失败:{message}",
|
||||
capabilities: "能力标签",
|
||||
fetchEnableThinking: "为获取的模型启用思考",
|
||||
capThinking: "思考",
|
||||
capAlwaysThinking: "始终思考",
|
||||
capImageIn: "图像输入",
|
||||
capVideoIn: "视频输入",
|
||||
capToolUse: "工具调用",
|
||||
customCapabilities: "其他能力(逗号分隔)",
|
||||
|
||||
// Languages
|
||||
language: "语言",
|
||||
|
||||
@@ -34,8 +34,6 @@ export interface Model {
|
||||
model: string;
|
||||
max_context_size: number;
|
||||
display_name: string | null;
|
||||
/** Optional model role, e.g. Sonnet/Opus/Fable/Haiku for Claude-style mapping. */
|
||||
role?: string | null;
|
||||
/** Whether the model declares extended context / thinking support. */
|
||||
supports_1m?: boolean;
|
||||
/** Agent-specific capability flags (e.g. Kimi Code `capabilities`). */
|
||||
|
||||
Reference in new issue
Block a user