docs: 新增界面截图,将 docs/superpowers 移出版本库

- 新增 docs/screenshots/ 5 张界面截图(仪表盘、会话、供应商列表、编辑供应商基本信息/模型映射)
- README.md / README_EN.md 新增截图章节与目录项,并在项目结构说明中补充 docs/screenshots/
- .gitignore 排除 docs/superpowers/,从索引中移除已跟踪的设计规范与实施计划文件(本地保留)
This commit is contained in:
954801926@qq.com committed 2026-07-28 18:57:54 +08:00
1 parent b1d137f072
commit 6f2b927914
15 files changed
+75 -4509

No files matched your search

+3
View File
@@ -10,3 +10,6 @@ src-tauri/wix314-binaries/
Thumbs.db
Snipaste_*.png
.worktrees/
# 内部设计规范与实施计划,仅供本地查阅,不入版本库
docs/superpowers/
+36 -1
View File
@@ -17,6 +17,7 @@
- [这是什么](#这是什么)
- [核心特性](#核心特性)
- [界面预览](#界面预览)
- [架构总览](#架构总览)
- [支持的供应商类型](#支持的供应商类型)
- [数据存储位置](#数据存储位置)
@@ -64,6 +65,38 @@ Kimi Switch 提供统一的图形界面:
| **会话管理** | 按工作区浏览、预览、归档、批量删除会话,逐行流式读取防崩溃(20MB 上限 + 500 字符折叠) |
| **未保存提示** | 关闭窗口前检测未保存修改,标题栏加 `*` 前缀 |
## 界面预览
**供应商列表**(浅色主题)
![供应商列表](docs/screenshots/providers.png)
清晰展示当前激活的供应商、默认模型、延迟和可用模型数量,支持快速切换、复制、跳转官网、编辑和删除操作。
**编辑供应商 - 基本信息**
![编辑供应商-基本信息](docs/screenshots/provider-basic-info.png)
包含供应商名称、备注、官网链接、托管供应商开关、API 格式、API Key 与请求地址等基础字段。
**编辑供应商 - 模型映射**
![编辑供应商-模型映射](docs/screenshots/provider-model-mapping.png)
一张表管理全部模型映射:显示名称、实际请求模型、上下文长度、1M 上下文声明、能力标签(思考/图像/视频/工具调用/其他),支持「一键设置」「获取模型列表」批量填充。
**用量仪表盘**
![用量仪表盘](docs/screenshots/dashboard.png)
8 项核心 KPI(请求数、非缓存输入、输出、缓存读/写/命中、总 Token、预估费用)+ 全年热力图 + 每日用量趋势(按模型分色堆叠柱状图)+ 模型用量明细(请求、Token、缓存命中、费用)。
**会话管理**
![会话管理](docs/screenshots/sessions.png)
按工作区隔离浏览 Kimi Code 会话,支持活跃/已归档/全部筛选,流式逐行预览会话内容(20MB 字节上限,500 字符折叠),可归档或批量删除。
## 架构总览
```
@@ -209,7 +242,9 @@ npm run dev
│
├── scripts/generate-icons.py # 从 SVG 生成各尺寸图标
├── public/kimi.svg # 应用图标源(蓝紫渐变 π)
└── docs/superpowers/ # 设计规范与实施计划
├── docs/
│ ├── screenshots/ # README 引用的界面截图
│ └── superpowers/ # 设计规范与实施计划
```
### Tauri Commands(前端 ↔ 后端)
+36 -1
View File
@@ -17,6 +17,7 @@
- [What Is This](#what-is-this)
- [Key Features](#key-features)
- [Screenshots](#screenshots)
- [Architecture Overview](#architecture-overview)
- [Supported Provider Types](#supported-provider-types)
- [Data Storage Locations](#data-storage-locations)
@@ -63,6 +64,38 @@ Kimi Switch provides a unified GUI:
| **Validation (reserved)** | i18n error strings are defined (duplicate names, missing credentials, missing Vertex fields, etc.), but the backend `validators.rs` is a stub and not yet wired |
| **Unsaved-changes prompt** | Detects unsaved edits before closing the window and prefixes the title bar with `*` |
## Screenshots
**Provider list** (light theme)
![Provider list](docs/screenshots/providers.png)
Shows the active provider, default model, latency and model count at a glance, with quick-switch, copy, open-website, edit and delete actions.
**Edit provider — basic info**
![Edit provider — basic info](docs/screenshots/provider-basic-info.png)
Provider name, notes, official URL, managed-provider toggle, API format, API key and base URL — the essentials in one panel.
**Edit provider — model mapping**
![Edit provider — model mapping](docs/screenshots/provider-model-mapping.png)
A single table for all model mappings: display name, real model ID, context length, 1M-context flag, capability tags (thinking / image / video / tools / other). One-click setup and "fetch model list" actions are available.
**Usage dashboard**
![Usage dashboard](docs/screenshots/dashboard.png)
Eight core KPIs (requests, non-cached input, output, cache read / write / hit, total tokens, estimated cost), a full-year heatmap, a per-model stacked daily usage bar chart, and a per-model usage breakdown (requests, tokens, cache hit, cost).
**Session management**
![Session management](docs/screenshots/sessions.png)
Browse Kimi Code sessions per workspace, filter by active / archived / all, stream-preview contents (20MB byte cap, 500-char collapse), archive or bulk-delete.
## Architecture Overview
```
@@ -208,7 +241,9 @@ Good for pure UI debugging.
│
├── scripts/generate-icons.py # Generate all icon sizes from SVG
├── public/kimi.svg # App icon source (blue-purple gradient π)
└── docs/superpowers/ # Design specs & implementation plans
├── docs/
│ ├── screenshots/ # UI screenshots referenced by the README
│ └── superpowers/ # Design specs & implementation plans
```
### Tauri commands (frontend ↔ backend)
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

File diff suppressed because it is too large. Load diff
@@ -1,270 +0,0 @@
# i18n / Agent Switch / UI Cleanup Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Complete Chinese/English i18n, replace the target dropdown with a CC Switch-style icon selector, clean up the header/footer layout, fix backend validation so managed providers and half-empty models don’t block saving, and ensure the app only fetches model lists on explicit user action.
**Architecture:** Keep the existing Rust backend commands and `ConfigTarget` abstraction. Front-end state stays in `App.tsx`; UI pieces live in `src/components/`. Validation/sanitization moves into a small Rust helper that normalizes config before validation, so legacy or in-progress configs can be saved without surprising the user.
**Tech Stack:** Tauri v2 (Rust), React + Tailwind CSS, TypeScript, `toml_edit`, `serde_yaml`.
---
### Task 1: Fix backend validation / sanitization
**Files:**
- Modify: `src-tauri/src/validators.rs`
- Modify: `src-tauri/src/commands.rs`
- Test: `src-tauri/tests/validators.rs`
- [ ] **Step 1.1: Treat `managed:*` providers as managed**
In `validators.rs`, add a helper and use it in the credential check:
```rust
fn is_managed_provider(provider: &Provider) -> bool {
provider.managed || provider.name.starts_with("managed:")
}
```
Replace the credential guard condition:
```rust
if !is_managed_provider(provider) && !matches!(provider.provider_type, ProviderType::Vertexai) {
// existing ambiguous / missing checks
}
```
- [ ] **Step 1.2: Sanitize config before validation**
In `commands.rs`, add a `sanitize_config(config: &mut Config)` function before `validate_config` is called in `save_config_command`:
```rust
fn sanitize_config(config: &mut Config) {
// Managed:* providers should carry the managed flag.
for provider in config.providers.values_mut() {
if provider.name.starts_with("managed:") {
provider.managed = true;
}
}
let first_provider = config.providers.keys().next().cloned();
for model in config.models.values_mut() {
if model.provider.trim().is_empty() {
if let Some(name) = &first_provider {
model.provider = name.clone();
}
}
if model.model.trim().is_empty() {
model.model = model.alias.clone();
}
if model.max_context_size == 0 {
model.max_context_size = 262_144;
}
}
}
```
Call it at the top of `save_config_command`:
```rust
pub fn save_config_command(
app: tauri::AppHandle,
target: ConfigTarget,
mut config: Config,
) -> Result<(), String> {
sanitize_config(&mut config);
if let Err(errors) = validate_config(&config) { ... }
...
}
```
- [ ] **Step 1.3: Add/update tests**
Add tests in `src-tauri/tests/validators.rs` for:
1. `managed:kimi-code` with empty credentials passes.
2. `glm-5` with empty provider/model/max_context_size is sanitized and then passes.
Run: `cd src-tauri && cargo test`
Expected: all tests pass.
---
### Task 2: Remove implicit model auto-fetch
**Files:**
- Modify: `src/components/ProviderEdit.tsx`
- [ ] **Step 2.1: Delete the auto-fetch `useEffect`**
Remove:
```tsx
const [autoFetched, setAutoFetched] = useState(false);
useEffect(() => {
if (models.length === 0 && !autoFetched && !discovering) {
setAutoFetched(true);
handleDiscover();
}
}, [models.length, autoFetched, discovering, handleDiscover]);
```
Keep the explicit `获取模型列表` button and the discovered-models panel. Models are now fetched only when the user clicks the button.
---
### Task 3: Redesign header and footer
**Files:**
- Modify: `src/App.tsx`
- Modify: `src/i18n/zh.ts`
- Modify: `src/i18n/en.ts`
- Modify: `src/index.css` (if needed for footer)
- [ ] **Step 3.1: Replace target dropdown with icon selector**
In `App.tsx`, replace the `<select>` for `target` with a row of icon buttons:
```tsx
function AgentSelector({ target, onChange }: { target: ConfigTarget; onChange: (t: ConfigTarget) => void }) {
const { t } = useTranslation();
const agents: { key: ConfigTarget; label: string; icon: string }[] = [
{ key: "kimi", label: t("targetKimi"), icon: "K" },
{ key: "omp", label: t("targetOmp"), icon: "O" },
];
return (
<div className="flex items-center gap-1 bg-[#1f1f23] border border-[#2a2a2e] rounded-lg p-1">
{agents.map((a) => (
<button
key={a.key}
type="button"
title={a.label}
onClick={() => onChange(a.key)}
className={`flex items-center gap-2 px-3 py-1.5 text-sm rounded-md transition-colors ${
target === a.key
? "bg-blue-600 text-white"
: "text-gray-400 hover:text-[#e5e5e7] hover:bg-[#2a2a2e]"
}`}
>
<span className="w-5 h-5 rounded-full bg-current/20 flex items-center justify-center text-xs font-bold">
{a.icon}
</span>
{a.label}
</button>
))}
</div>
);
}
```
Use `<AgentSelector target={target} onChange={handleSetTarget} />` in the header.
- [ ] **Step 3.2: Move global actions to a footer bar**
Render a footer below `<main>`:
```tsx
<footer className="flex items-center justify-between px-4 py-2 border-t border-[#2a2a2e] bg-[#16161a] text-sm">
<div className="flex items-center gap-2">
{dirty && <span className="text-orange-400">● {t("unsavedChanges")}</span>}
</div>
<div className="flex items-center gap-2">
<button ... onClick={save} disabled={!dirty}>{t("saveConfig")}</button>
<button ... onClick={refresh}>{t("reloadConfig")}</button>
<button ... onClick={openConfigDir}>{t("openConfigDir")}</button>
</div>
</footer>
```
Remove the corresponding buttons from the top header.
- [ ] **Step 3.3: Keep language switch in header**
Keep the language `<select>` in the top-right of the header, next to the agent selector.
- [ ] **Step 3.4: Add missing translation keys**
Add `unsavedChanges: "未保存修改"` / `"Unsaved changes"` to both translation files.
---
### Task 4: Improve adaptive / maximize-friendly layout
**Files:**
- Modify: `src/components/ProviderList.tsx`
- Modify: `src/components/ProviderEdit.tsx`
- [ ] **Step 4.1: ProviderList cards adapt to width**
Ensure the list container is `flex-1 overflow-auto` and cards use `w-full`. The right-side action buttons should wrap on narrow widths by adding `flex-wrap` to their container.
- [ ] **Step 4.2: ProviderEdit table scrolls horizontally**
Wrap the model mapping table in a `div` with `overflow-auto`:
```tsx
<div className="overflow-auto">
<table className="w-full min-w-[640px] text-sm">...</table>
</div>
```
Change the two-column basic-info grid to collapse on small widths by using `grid-cols-1 md:grid-cols-2`.
---
### Task 5: Complete zh/en i18n
**Files:**
- Modify: `src/i18n/zh.ts`
- Modify: `src/i18n/en.ts`
- [ ] **Step 5.1: Audit for hardcoded strings**
Search `src/components/` and `src/App.tsx` for any literal Chinese or English UI text not using `t()`. Add keys and replace them. Ensure keys added to both `zh.ts` and `en.ts`.
---
### Task 6: Rename / update logo and icons
**Files:**
- Modify: `index.html`
- Create/overwrite: `public/kimi.svg`
- Create/overwrite: `src-tauri/icons/32x32.png`, `src-tauri/icons/128x128.png`, `src-tauri/icons/128x128@2x.png`, `src-tauri/icons/icon.ico`
- [ ] **Step 6.1: Generate Kimi Switch icons**
Use a Python script (Pillow) to generate PNG/ICO files with a π symbol. Update `index.html` favicon to `/kimi.svg`.
- [ ] **Step 6.2: Verify app title**
`tauri.conf.json` already has `"title": "Kimi Switch"`. Ensure `index.html` `<title>` is also `Kimi Switch`.
---
### Task 7: Build and run
**Files:** N/A
- [ ] **Step 7.1: Run frontend build**
`npm run build`
Expected: no TypeScript or build errors.
- [ ] **Step 7.2: Run Rust tests**
`cd src-tauri && cargo test`
Expected: all tests pass.
- [ ] **Step 7.3: Start dev app**
`npm run tauri-dev`
Expected: app window opens, agent switch works, save/reload/open buttons work, model fetch is manual.
---
### Self-Review
- **Spec coverage:** managed-provider error → Task 1.1; glm-5 empty fields → Task 1.2; manual model fetch → Task 2; CC Switch icon selector → Task 3.1; footer action placement → Task 3.2; adaptive layout → Task 4; i18n → Task 5; logo/title → Task 6.
- **Placeholder scan:** no TBD/TODO/fill-in details; each step has file paths and code.
- **Type consistency:** `ConfigTarget` values remain `"kimi"` / `"omp"`; translation keys match existing `TranslationKey` type.
@@ -1,192 +0,0 @@
# Kimi Code /reload Hint Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add an info icon tooltip next to the "switch provider" button, visible only in the Kimi Code tab, reminding users to run `/reload` after switching providers.
**Architecture:** Extend `ProviderList` to receive the current `agent` and conditionally render a hint icon beside the switch button. Keep changes minimal and UI-only; no state or backend changes.
**Tech Stack:** React 18, TypeScript, Tailwind CSS, custom i18n module (`src/i18n`).
---
### Task 1: Add i18n translation keys
**Files:**
- Modify: `src/i18n/zh.ts`
- Modify: `src/i18n/en.ts`
- [ ] **Step 1: Add Chinese translation**
Insert `switchReloadHint` into the "Provider list" section of `src/i18n/zh.ts`, e.g. after `switchTo`:
```ts
switchReloadHint: "切换供应商后,请在 Kimi Code 中执行 /reload 以生效。",
```
- [ ] **Step 2: Add English translation**
Insert the matching key into `src/i18n/en.ts`:
```ts
switchReloadHint: "After switching providers, run /reload in Kimi Code to apply the change.",
```
- [ ] **Step 3: Commit**
```bash
git add src/i18n/zh.ts src/i18n/en.ts
git commit -m "i18n: add switchReloadHint for Kimi Code reload reminder"
```
---
### Task 2: Update ProviderList to accept agent and render hint
**Files:**
- Modify: `src/components/ProviderList.tsx`
- [ ] **Step 1: Import Agent type and update props**
Add `Agent` to the import from `../types` and add `agent` to `ProviderListProps`:
```tsx
import { useTranslation } from "../i18n";
import type { Agent, Model, Provider } from "../types";
interface ProviderListProps {
providers: Provider[];
defaultModel: string | null;
models: Record<string, Model>;
onEdit: (name: string) => void;
onDelete: (name: string) => void;
onAdd: () => void;
onSwitchProvider: (name: string) => void;
agent: Agent;
}
```
- [ ] **Step 2: Destructure agent in component signature**
```tsx
export function ProviderList({
providers,
defaultModel,
models,
onEdit,
onDelete,
onAdd,
onSwitchProvider,
agent,
}: ProviderListProps) {
```
- [ ] **Step 3: Render hint icon next to the switch button**
In the supplier card actions area, place an info icon immediately after the "切换使用" / "Switch to" button, rendered only when `agent === "kimi_code"`:
```tsx
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onSwitchProvider(provider.name);
}}
className={`px-3 py-1.5 text-sm rounded focus:ring-2 focus:outline-none ${
isActive
? "bg-green-600 hover:bg-green-700 text-white focus:ring-green-500"
: "bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500"
}`}
>
{isActive ? t("inUse") : t("switchTo")}
</button>
{agent === "kimi_code" && (
<span
className="text-sm text-gray-500 hover:text-gray-300 cursor-help select-none"
title={t("switchReloadHint")}
aria-label={t("switchReloadHint")}
>
ⓘ
</span>
)}
```
- [ ] **Step 4: Commit**
```bash
git add src/components/ProviderList.tsx
git commit -m "feat: show /reload hint next to switch provider button in Kimi Code tab"
```
---
### Task 3: Pass agent from App to ProviderList
**Files:**
- Modify: `src/App.tsx`
- [ ] **Step 1: Add agent prop to ProviderList usage**
Locate the `<ProviderList ... />` call in `src/App.tsx` and add `agent={agent}`:
```tsx
<ProviderList
providers={providers}
defaultModel={config.default_model}
models={config.models}
onEdit={(name) => {
setEditingProvider(name);
setView("edit");
}}
onDelete={handleDeleteProvider}
onAdd={handleAddProvider}
onSwitchProvider={handleSwitchProvider}
agent={agent}
/>
```
- [ ] **Step 2: Commit**
```bash
git add src/App.tsx
git commit -m "chore: pass current agent to ProviderList"
```
---
### Task 4: Verify with type check and manual test
- [ ] **Step 1: Run TypeScript compilation**
```bash
npm run build
```
Expected: `tsc` completes without errors and Vite builds successfully.
- [ ] **Step 2: Run the app**
```bash
npm run tauri-dev
```
- [ ] **Step 3: Manual verification**
1. Select the **Kimi Code** tab.
2. Hover over the `ⓘ` icon next to any provider's "切换使用" / "Switch to" button.
3. Confirm the tooltip reads:
- 中文:`切换供应商后,请在 Kimi Code 中执行 /reload 以生效。`
- English: `After switching providers, run /reload in Kimi Code to apply the change.`
4. Switch to the **Pi** tab and confirm the `ⓘ` icon is **not** present.
- [ ] **Step 4: Commit verification notes (optional)**
If any fixes were needed, commit them; otherwise no additional commit is required.
---
## Self-Review
- **Spec coverage:** Every design requirement (icon next to switch button, Kimi Code only, i18n, no new dependencies) maps to a task above.
- **Placeholder scan:** No TBD/TODO/fill-in-details present.
- **Type consistency:** `Agent` type is imported from `../types` and used consistently in props and conditional rendering.
@@ -1,937 +0,0 @@
# Kimi Code 全局配置面板与模型默认上下文 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a Kimi Code global settings panel inside ProviderEdit and auto-set `max_context_size` for new models based on a model-ID lookup table.
**Architecture:** Extend the shared `Config` type with typed agent settings, add two pure helper modules (`agent-settings.ts` for `raw_other` IO and `model-defaults.ts` for context lookup), and render the panel in a new React component wired through `ProviderEdit`.
**Tech Stack:** React 18, TypeScript, Tailwind CSS, custom i18n, Tauri v2 backend (no Rust changes).
---
### Task 1: Add agent settings types
**Files:**
- Modify: `src/types/index.ts`
- [ ] **Step 1: Append new types**
Insert at the end of `src/types/index.ts`:
```ts
export interface ThinkingConfig {
enabled?: boolean;
effort?: "low" | "medium" | "high" | "max";
keep?: "all" | false | 0 | "no" | "off" | "none" | null;
}
export interface LoopControlConfig {
max_retries_per_step?: number;
reserved_context_size?: number;
}
export interface BackgroundConfig {
max_running_tasks?: number;
keep_alive_on_exit?: boolean;
}
export interface PermissionRule {
decision?: "allow" | "deny" | "ask";
scope?: string;
pattern?: string;
reason?: string;
}
export interface Hook {
event?: string;
matcher?: string;
command?: string;
timeout?: number;
}
export interface AgentSettings {
thinking?: ThinkingConfig;
loop_control?: LoopControlConfig;
background?: BackgroundConfig;
permission?: { rules?: PermissionRule[] };
hooks?: Hook[];
}
```
- [ ] **Step 2: Commit**
```bash
git add src/types/index.ts
git commit -m "types: add AgentSettings and related Kimi Code config types"
```
---
### Task 2: Create model default context size lookup
**Files:**
- Create: `src/lib/model-defaults.ts`
- [ ] **Step 1: Create the lookup module**
Create `src/lib/model-defaults.ts`:
```ts
export const DEFAULT_MAX_CONTEXT_SIZE = 256000;
interface ModelContextRule {
pattern: RegExp;
max_context_size: number;
}
const MODEL_CONTEXT_RULES: ModelContextRule[] = [
// Kimi
{ pattern: /^kimi-for-coding$/i, max_context_size: 262144 },
{ pattern: /^kimi-k2\.5/i, max_context_size: 256000 },
{ pattern: /^kimi-k2/i, max_context_size: 256000 },
{ pattern: /^kimi-/i, max_context_size: 256000 },
// GLM
{ pattern: /^glm-5\.2/i, max_context_size: 1000000 },
{ pattern: /^glm-5\.1/i, max_context_size: 256000 },
{ pattern: /^glm-5/i, max_context_size: 256000 },
{ pattern: /^glm-4/i, max_context_size: 128000 },
{ pattern: /^glm-/i, max_context_size: 128000 },
// MiniMax
{ pattern: /^MiniMax-M3/i, max_context_size: 1000000 },
{ pattern: /^MiniMax-Text-01/i, max_context_size: 400000 },
{ pattern: /^MiniMax-/i, max_context_size: 256000 },
// Qwen
{ pattern: /^qwen2\.5/i, max_context_size: 128000 },
{ pattern: /^qwen-max/i, max_context_size: 128000 },
{ pattern: /^qwen-plus/i, max_context_size: 128000 },
{ pattern: /^qwen-turbo/i, max_context_size: 128000 },
{ pattern: /^qwen-coder/i, max_context_size: 128000 },
{ pattern: /^qwen-/i, max_context_size: 128000 },
// DeepSeek
{ pattern: /^deepseek-r1/i, max_context_size: 64000 },
{ pattern: /^deepseek-v3/i, max_context_size: 64000 },
{ pattern: /^deepseek-coder/i, max_context_size: 64000 },
{ pattern: /^deepseek-/i, max_context_size: 64000 },
// Hunyuan
{ pattern: /^hunyuan-pro/i, max_context_size: 32000 },
{ pattern: /^hunyuan-standard/i, max_context_size: 32000 },
{ pattern: /^hunyuan-lite/i, max_context_size: 32000 },
{ pattern: /^hunyuan-/i, max_context_size: 32000 },
// Doubao
{ pattern: /^doubao-pro/i, max_context_size: 128000 },
{ pattern: /^doubao-lite/i, max_context_size: 128000 },
{ pattern: /^doubao-vision/i, max_context_size: 128000 },
{ pattern: /^doubao-/i, max_context_size: 128000 },
// ERNIE
{ pattern: /^ernie-4\.0/i, max_context_size: 128000 },
{ pattern: /^ernie-3\.5/i, max_context_size: 128000 },
{ pattern: /^ernie-speed/i, max_context_size: 128000 },
{ pattern: /^ernie-lite/i, max_context_size: 128000 },
{ pattern: /^ernie-/i, max_context_size: 128000 },
// Spark
{ pattern: /^spark-v4/i, max_context_size: 32000 },
{ pattern: /^spark-v3\.5/i, max_context_size: 32000 },
{ pattern: /^spark-pro/i, max_context_size: 32000 },
{ pattern: /^spark-max/i, max_context_size: 32000 },
{ pattern: /^spark-/i, max_context_size: 32000 },
// SenseChat
{ pattern: /^sensechat-/i, max_context_size: 128000 },
// Baichuan
{ pattern: /^baichuan-4/i, max_context_size: 128000 },
{ pattern: /^baichuan-3/i, max_context_size: 128000 },
{ pattern: /^baichuan-/i, max_context_size: 128000 },
// Yi
{ pattern: /^yi-/i, max_context_size: 128000 },
// Claude
{ pattern: /^claude-opus/i, max_context_size: 200000 },
{ pattern: /^claude-sonnet/i, max_context_size: 200000 },
{ pattern: /^claude-haiku/i, max_context_size: 200000 },
{ pattern: /^claude-/i, max_context_size: 200000 },
// OpenAI
{ pattern: /^gpt-4\.1/i, max_context_size: 1047576 },
{ pattern: /^gpt-4o/i, max_context_size: 128000 },
{ pattern: /^gpt-4-turbo/i, max_context_size: 128000 },
{ pattern: /^gpt-4-/i, max_context_size: 128000 },
// Gemini
{ pattern: /^gemini-2\.0-flash/i, max_context_size: 1048576 },
{ pattern: /^gemini-1\.5-pro/i, max_context_size: 2097152 },
{ pattern: /^gemini-1\.5-flash/i, max_context_size: 1048576 },
{ pattern: /^gemini-/i, max_context_size: 1048576 },
];
export function getDefaultMaxContextSize(modelId: string): number {
for (const rule of MODEL_CONTEXT_RULES) {
if (rule.pattern.test(modelId)) {
return rule.max_context_size;
}
}
return DEFAULT_MAX_CONTEXT_SIZE;
}
```
- [ ] **Step 2: Commit**
```bash
git add src/lib/model-defaults.ts
git commit -m "feat: add model default max_context_size lookup table"
```
---
### Task 3: Create agent settings IO helpers
**Files:**
- Create: `src/lib/agent-settings.ts`
- [ ] **Step 1: Create helper module**
Create `src/lib/agent-settings.ts`:
```ts
import type { AgentSettings } from "../types";
const DEFAULT_SETTINGS: AgentSettings = {
thinking: {
enabled: true,
effort: "medium",
keep: "all",
},
loop_control: {
max_retries_per_step: 3,
reserved_context_size: 50000,
},
background: {
keep_alive_on_exit: false,
},
permission: { rules: [] },
hooks: [],
};
function asRecord(value: unknown): Record<string, unknown> {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value as Record<string, unknown>;
}
return {};
}
function getSection<T>(rawOther: unknown, key: string): T | undefined {
const root = asRecord(rawOther);
const section = root[key];
if (section === undefined || section === null) return undefined;
return section as T;
}
export function getAgentSettings(rawOther: unknown): AgentSettings {
return {
thinking: {
...DEFAULT_SETTINGS.thinking,
...getSection<AgentSettings["thinking"]>(rawOther, "thinking"),
},
loop_control: {
...DEFAULT_SETTINGS.loop_control,
...getSection<AgentSettings["loop_control"]>(rawOther, "loop_control"),
},
background: {
...DEFAULT_SETTINGS.background,
...getSection<AgentSettings["background"]>(rawOther, "background"),
},
permission: {
rules: getSection<AgentSettings["permission"]>(rawOther, "permission")?.rules ?? [],
},
hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [],
};
}
export function setAgentSettings(
rawOther: unknown,
patch: Partial<AgentSettings>
): unknown {
const root = { ...asRecord(rawOther) };
const current = getAgentSettings(rawOther);
const next: AgentSettings = {
thinking: { ...current.thinking, ...patch.thinking },
loop_control: { ...current.loop_control, ...patch.loop_control },
background: { ...current.background, ...patch.background },
permission: { rules: patch.permission?.rules ?? current.permission?.rules ?? [] },
hooks: patch.hooks ?? current.hooks ?? [],
};
if (next.thinking) root.thinking = next.thinking;
if (next.loop_control) root.loop_control = next.loop_control;
if (next.background) root.background = next.background;
if (next.permission?.rules && next.permission.rules.length > 0) {
root.permission = next.permission;
} else {
delete root.permission;
}
if (next.hooks && next.hooks.length > 0) {
root.hooks = next.hooks;
} else {
delete root.hooks;
}
return root;
}
```
- [ ] **Step 2: Commit**
```bash
git add src/lib/agent-settings.ts
git commit -m "feat: add agent settings read/write helpers"
```
---
### Task 4: Add i18n keys
**Files:**
- Modify: `src/i18n/zh.ts`
- Modify: `src/i18n/en.ts`
- [ ] **Step 1: Add Chinese keys**
Insert into `src/i18n/zh.ts` in the "Agent / target" section:
```ts
agentSettings: "全局配置",
enableThinking: "启用思考",
thinkingLevel: "思考等级",
thinkingKeep: "保留思考内容",
thinkingLow: "低",
thinkingMedium: "中",
thinkingHigh: "高",
thinkingMax: "最大",
thinkingContextHint: "启用思考会占用更多上下文,请确保模型上下文长度和预留空间足够。",
loopControlSettings: "循环控制",
maxRetriesPerStep: "单步重试次数",
reservedContextSize: "上下文预留大小",
backgroundSettings: "后台任务",
maxRunningTasks: "最大并发数",
keepAliveOnExit: "退出时保持运行",
permissionRules: "权限规则",
permissionDecision: "处置",
permissionPattern: "模式",
permissionAllow: "允许",
permissionDeny: "拒绝",
permissionAsk: "询问",
addRule: "+ 添加规则",
addCommonRules: "添加常用规则",
hooks: "生命周期钩子",
hookEvent: "事件",
hookMatcher: "匹配器",
hookCommand: "命令",
hookTimeout: "超时",
addHook: "+ 添加钩子",
```
- [ ] **Step 2: Add English keys**
Insert matching keys into `src/i18n/en.ts`:
```ts
agentSettings: "Global Settings",
enableThinking: "Enable thinking",
thinkingLevel: "Thinking level",
thinkingKeep: "Keep thinking content",
thinkingLow: "Low",
thinkingMedium: "Medium",
thinkingHigh: "High",
thinkingMax: "Max",
thinkingContextHint: "Thinking uses more context. Ensure the model context length and reserved size are sufficient.",
loopControlSettings: "Loop Control",
maxRetriesPerStep: "Max retries per step",
reservedContextSize: "Reserved context size",
backgroundSettings: "Background Tasks",
maxRunningTasks: "Max running tasks",
keepAliveOnExit: "Keep alive on exit",
permissionRules: "Permission Rules",
permissionDecision: "Decision",
permissionPattern: "Pattern",
permissionAllow: "Allow",
permissionDeny: "Deny",
permissionAsk: "Ask",
addRule: "+ Add rule",
addCommonRules: "Add common rules",
hooks: "Lifecycle Hooks",
hookEvent: "Event",
hookMatcher: "Matcher",
hookCommand: "Command",
hookTimeout: "Timeout",
addHook: "+ Add hook",
```
- [ ] **Step 3: Commit**
```bash
git add src/i18n/zh.ts src/i18n/en.ts
git commit -m "i18n: add agent settings panel translations"
```
---
### Task 5: Build the AgentSettingsPanel component
**Files:**
- Create: `src/components/AgentSettingsPanel.tsx`
- [ ] **Step 1: Create the panel component**
Create `src/components/AgentSettingsPanel.tsx`:
```tsx
import { useTranslation } from "../i18n";
import { getAgentSettings, setAgentSettings } from "../lib/agent-settings";
import type { AgentSettings, PermissionRule, Hook } from "../types";
import type { ReactNode } from "react";
import type { TranslationKey } from "../i18n/zh";
interface AgentSettingsPanelProps {
rawOther: unknown;
onChange: (nextRawOther: unknown) => void;
}
const THINKING_LEVELS = ["low", "medium", "high", "max"] as const;
const THINKING_LABELS: Record<(typeof THINKING_LEVELS)[number], TranslationKey> = {
low: "thinkingLow",
medium: "thinkingMedium",
high: "thinkingHigh",
max: "thinkingMax",
};
const PERMISSION_DECISIONS = ["allow", "deny", "ask"] as const;
const PERMISSION_LABELS: Record<(typeof PERMISSION_DECISIONS)[number], TranslationKey> = {
allow: "permissionAllow",
deny: "permissionDeny",
ask: "permissionAsk",
};
const COMMON_EVENTS = ["PreToolUse", "PostToolUse"] as const;
export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelProps) {
const { t } = useTranslation();
const settings = getAgentSettings(rawOther);
const update = (patch: Partial<AgentSettings>) => {
onChange(setAgentSettings(rawOther, patch));
};
const updateThinking = (patch: Partial<AgentSettings["thinking"]>) => {
update({ thinking: { ...settings.thinking, ...patch } });
};
const updateLoopControl = (patch: Partial<AgentSettings["loop_control"]>) => {
update({ loop_control: { ...settings.loop_control, ...patch } });
};
const updateBackground = (patch: Partial<AgentSettings["background"]>) => {
update({ background: { ...settings.background, ...patch } });
};
const setRules = (rules: PermissionRule[]) => {
update({ permission: { rules } });
};
const setHooks = (hooks: Hook[]) => {
update({ hooks });
};
const thinkingEnabled = settings.thinking?.enabled ?? true;
return (
<div className="mt-6 space-y-4">
<h3 className="text-gray-400 text-sm font-medium">{t("agentSettings")}</h3>
{/* Thinking */}
<Card title={t("enableThinking")}>
<Checkbox
label={t("enableThinking")}
checked={thinkingEnabled}
onChange={(checked) => updateThinking({ enabled: checked })}
/>
<div className="flex items-center gap-3 flex-wrap">
<span className="text-sm text-gray-400">{t("thinkingLevel")}</span>
<Segmented
options={THINKING_LEVELS.map((lvl) => ({ key: lvl, label: t(THINKING_LABELS[lvl]) }))}
value={settings.thinking?.effort ?? "medium"}
onChange={(effort) => updateThinking({ effort })}
disabled={!thinkingEnabled}
/>
</div>
<Checkbox
label={t("thinkingKeep")}
checked={settings.thinking?.keep === "all"}
disabled={!thinkingEnabled}
onChange={(checked) => updateThinking({ keep: checked ? "all" : false })}
/>
<p className="text-xs text-gray-500">{t("thinkingContextHint")}</p>
</Card>
{/* Loop Control */}
<Card title={t("loopControlSettings")}>
<NumberField
label={t("maxRetriesPerStep")}
value={settings.loop_control?.max_retries_per_step ?? 3}
onChange={(v) => updateLoopControl({ max_retries_per_step: v })}
/>
<NumberField
label={t("reservedContextSize")}
value={settings.loop_control?.reserved_context_size ?? 50000}
onChange={(v) => updateLoopControl({ reserved_context_size: v })}
/>
</Card>
{/* Background */}
<Card title={t("backgroundSettings")}>
<NumberField
label={t("maxRunningTasks")}
value={settings.background?.max_running_tasks}
onChange={(v) => updateBackground({ max_running_tasks: v })}
/>
<Checkbox
label={t("keepAliveOnExit")}
checked={settings.background?.keep_alive_on_exit ?? false}
onChange={(checked) => updateBackground({ keep_alive_on_exit: checked })}
/>
</Card>
{/* Permission Rules */}
<Card title={t("permissionRules")}>
<div className="space-y-2">
{(settings.permission?.rules ?? []).map((rule, idx) => (
<div key={idx} className="flex items-center gap-2">
<select
className="bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
value={rule.decision ?? "allow"}
onChange={(e) => {
const rules = [...(settings.permission?.rules ?? [])];
rules[idx] = { ...rule, decision: e.target.value as PermissionRule["decision"] };
setRules(rules);
}}
>
{PERMISSION_DECISIONS.map((d) => (
<option key={d} value={d}>{t(PERMISSION_LABELS[d])}</option>
))}
</select>
<input
type="text"
className="flex-1 min-w-0 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
value={rule.pattern ?? ""}
placeholder={t("permissionPattern")}
onChange={(e) => {
const rules = [...(settings.permission?.rules ?? [])];
rules[idx] = { ...rule, pattern: e.target.value };
setRules(rules);
}}
/>
<button
type="button"
onClick={() => {
const rules = [...(settings.permission?.rules ?? [])];
rules.splice(idx, 1);
setRules(rules);
}}
className="px-2 py-1 text-sm text-red-400 hover:bg-red-900/20 rounded"
>
×
</button>
</div>
))}
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
setRules([...(settings.permission?.rules ?? []), { decision: "allow", pattern: "" }]);
}}
className="px-3 py-1.5 text-sm border border-[#2a2a2e] rounded hover:bg-[#252529]"
>
{t("addRule")}
</button>
<button
type="button"
onClick={() => {
setRules([
...(settings.permission?.rules ?? []),
{ decision: "allow", pattern: "Read" },
{ decision: "deny", pattern: "Bash(rm -rf*)" },
]);
}}
className="px-3 py-1.5 text-sm border border-[#2a2a2e] rounded hover:bg-[#252529]"
>
{t("addCommonRules")}
</button>
</div>
</Card>
{/* Hooks */}
<Card title={t("hooks")}>
<div className="space-y-2">
{(settings.hooks ?? []).map((hook, idx) => (
<div key={idx} className="flex items-center gap-2 flex-wrap">
<input
list="hook-events"
type="text"
className="w-32 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
value={hook.event ?? ""}
placeholder={t("hookEvent")}
onChange={(e) => {
const hooks = [...(settings.hooks ?? [])];
hooks[idx] = { ...hook, event: e.target.value };
setHooks(hooks);
}}
/>
<datalist id="hook-events">
{COMMON_EVENTS.map((e) => <option key={e} value={e} />)}
</datalist>
<input
type="text"
className="w-24 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
value={hook.matcher ?? ""}
placeholder={t("hookMatcher")}
onChange={(e) => {
const hooks = [...(settings.hooks ?? [])];
hooks[idx] = { ...hook, matcher: e.target.value };
setHooks(hooks);
}}
/>
<input
type="text"
className="flex-1 min-w-0 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
value={hook.command ?? ""}
placeholder={t("hookCommand")}
onChange={(e) => {
const hooks = [...(settings.hooks ?? [])];
hooks[idx] = { ...hook, command: e.target.value };
setHooks(hooks);
}}
/>
<input
type="number"
className="w-20 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
value={hook.timeout ?? ""}
placeholder={t("hookTimeout")}
onChange={(e) => {
const hooks = [...(settings.hooks ?? [])];
hooks[idx] = { ...hook, timeout: e.target.value === "" ? undefined : Number(e.target.value) };
setHooks(hooks);
}}
/>
<button
type="button"
onClick={() => {
const hooks = [...(settings.hooks ?? [])];
hooks.splice(idx, 1);
setHooks(hooks);
}}
className="px-2 py-1 text-sm text-red-400 hover:bg-red-900/20 rounded"
>
×
</button>
</div>
))}
</div>
<button
type="button"
onClick={() => {
setHooks([...(settings.hooks ?? []), { event: "", matcher: "", command: "" }]);
}}
className="px-3 py-1.5 text-sm border border-[#2a2a2e] rounded hover:bg-[#252529]"
>
{t("addHook")}
</button>
</Card>
</div>
);
}
function Card({ title, children }: { title: string; children: ReactNode }) {
return (
<div className="bg-[#16161a] border border-[#2a2a2e] rounded-xl p-4 space-y-3">
<h4 className="text-gray-400 text-sm font-medium">{title}</h4>
{children}
</div>
);
}
function Checkbox({
label,
checked,
disabled,
onChange,
}: {
label: string;
checked: boolean;
disabled?: boolean;
onChange: (checked: boolean) => void;
}) {
return (
<label className={`flex items-center gap-2 text-sm ${disabled ? "text-gray-600" : "text-[#e5e5e7]"}`}>
<input
type="checkbox"
checked={checked}
disabled={disabled}
onChange={(e) => onChange(e.target.checked)}
className="rounded border-[#2a2a2e] bg-[#1f1f23] text-blue-600 focus:ring-blue-500"
/>
{label}
</label>
);
}
function NumberField({
label,
value,
onChange,
}: {
label: string;
value?: number;
onChange: (value: number | undefined) => void;
}) {
return (
<label className="flex items-center gap-3 text-sm text-[#e5e5e7]">
<span className="text-gray-400">{label}</span>
<input
type="number"
value={value ?? ""}
onChange={(e) => {
const raw = e.target.value;
onChange(raw === "" ? undefined : Number(raw));
}}
className="w-32 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
</label>
);
}
function Segmented({
options,
value,
onChange,
disabled,
}: {
options: { key: string; label: string }[];
value: string;
onChange: (value: string) => void;
disabled?: boolean;
}) {
return (
<div className={`flex items-center rounded overflow-hidden border border-[#2a2a2e] ${disabled ? "opacity-50" : ""}`}>
{options.map((opt) => (
<button
key={opt.key}
type="button"
disabled={disabled}
onClick={() => onChange(opt.key)}
className={`px-3 py-1 text-sm ${
value === opt.key
? "bg-blue-600 text-white"
: "bg-[#1f1f23] text-gray-400 hover:bg-[#252529]"
}`}
>
{opt.label}
</button>
))}
</div>
);
}
```
- [ ] **Step 2: Commit**
```bash
git add src/components/AgentSettingsPanel.tsx
git commit -m "feat: add AgentSettingsPanel component"
```
---
### Task 6: Wire AgentSettingsPanel into ProviderEdit
**Files:**
- Modify: `src/components/ProviderEdit.tsx`
- [ ] **Step 1: Update ProviderEditProps**
Add `rawOther` and `onRawOtherChange` to the props interface:
```tsx
interface ProviderEditProps {
agent: Agent;
provider: Provider;
models: Model[];
defaultModel: string | null;
rawOther: unknown;
onRawOtherChange: (nextRawOther: unknown) => void;
onBack: () => void;
// ... existing callbacks
}
```
- [ ] **Step 2: Destructure new props**
```tsx
export function ProviderEdit({
agent,
provider,
models,
defaultModel,
rawOther,
onRawOtherChange,
onBack,
// ... existing
}: ProviderEditProps) {
```
- [ ] **Step 3: Render the panel in the model mapping tab**
Locate the model mapping tab content (around the model table and "+ 添加模型映射" button) and insert `<AgentSettingsPanel />` at the bottom, conditionally for Kimi Code:
```tsx
{agent === "kimi_code" && (
<AgentSettingsPanel rawOther={rawOther} onChange={onRawOtherChange} />
)}
```
- [ ] **Step 4: Commit**
```bash
git add src/components/ProviderEdit.tsx
git commit -m "feat: render AgentSettingsPanel in ProviderEdit for Kimi Code"
```
---
### Task 7: Wire ProviderEdit to App.tsx
**Files:**
- Modify: `src/App.tsx`
- [ ] **Step 1: Pass rawOther and updater to ProviderEdit**
Find the `<ProviderEdit ... />` call and add:
```tsx
<ProviderEdit
agent={agent}
provider={currentProvider}
models={...}
defaultModel={config.default_model}
rawOther={config.raw_other}
onRawOtherChange={(nextRawOther) =>
updateConfig((cfg) => ({ ...cfg, raw_other: nextRawOther }))
}
// ... existing props
/>
```
- [ ] **Step 2: Commit**
```bash
git add src/App.tsx
git commit -m "chore: pass rawOther to ProviderEdit"
```
---
### Task 8: Apply smart max_context_size defaults when adding models
**Files:**
- Modify: `src/App.tsx`
- [ ] **Step 1: Import helper in App.tsx**
In `src/App.tsx`, import:
```tsx
import { getDefaultMaxContextSize } from "./lib/model-defaults";
```
- [ ] **Step 2: Use lookup in onModelAdd**
Find the `onModelAdd` prop passed to `<ProviderEdit />` and update the default `max_context_size`:
```tsx
onModelAdd={() => {
const providerModels = Object.values(config.models).filter(
(m) => m.provider === currentProvider.name
);
const alias = `${currentProvider.name}-${providerModels.length + 1}`;
updateConfig((cfg) => ({
...cfg,
models: {
...cfg.models,
[alias]: {
alias,
provider: currentProvider.name,
model: "",
max_context_size: getDefaultMaxContextSize(alias),
display_name: null,
role: null,
supports_1m: false,
capabilities: [],
},
},
}));
}}
```
- [ ] **Step 3: Use lookup in handleApplyProviderJson**
Update the model creation loop in `handleApplyProviderJson`:
```tsx
updatedModels[alias] = {
...m,
alias,
provider: provider.name,
max_context_size: m.max_context_size ?? getDefaultMaxContextSize(m.model),
};
```
- [ ] **Step 4: Commit**
```bash
git add src/App.tsx
git commit -m "feat: apply smart default max_context_size when adding models"
```
---
### Task 9: Verify with build and manual checks
- [ ] **Step 1: Type check**
```bash
npm run build
```
Expected: `tsc` and Vite build succeed.
- [ ] **Step 2: Run the app**
```bash
npm run tauri-dev
```
- [ ] **Step 3: Manual verification**
1. Kimi Code tab → edit a provider → "模型映射" tab → scroll to bottom.
2. Confirm "全局配置" panel shows 5 cards.
3. Toggle "启用思考" and verify effort/keep enable/disable.
4. Change thinking effort, save, open `~/.kimi-code/config.toml`, verify `[thinking]` block.
5. Add a permission rule and a hook, save, verify `[[permission.rules]]` and `[[hooks]]` in TOML.
6. Add a new model with alias `glm-5.2-xxx`, verify `max_context_size` defaults to `1000000`.
7. Add a new model with unknown alias, verify `max_context_size` defaults to `256000`.
8. Switch to Pi tab, confirm global settings panel is not visible.
- [ ] **Step 4: Commit any fixes**
If any fixes were needed during verification, commit them.
---
## Self-Review
- **Spec coverage:** Every requirement (5 cards, model defaults, i18n, Kimi-only visibility, manual override) maps to a task.
- **Placeholder scan:** No TBD/TODO/fill-in-details; all code is shown.
- **Type consistency:** `AgentSettings`, helper signatures, and component props match across tasks.
@@ -1,420 +0,0 @@
# KimiSwitch 设计文档
## 1. 项目概述
KimiSwitch 是一个 Windows 桌面配置管理工具,用于简化 Kimi Code CLI 的多供应商配置维护。它通过图形界面让用户增删改查 `[providers]` 与 `[models]`,支持多套配置**配置文件(Profile)**的保存与一键切换,最终生成符合 Kimi Code CLI 规范的 `~/.kimi-code/config.toml`。
## 2. 问题与目标
### 2.1 现状痛点
- Kimi Code CLI 的配置文件是 TOML 格式,需要手动编辑。
- 维护多个供应商(Kimi、OpenAI、Anthropic、Gemini、Vertex 等)时,`base_url` 与 `api_key` 容易写错或重复。
- 对 TOML 语法不熟的用户(例如 `[models."gpt-4.1"]` 需要引号)上手成本高。
- 工作、个人、测试等场景需要多套 API 配置时,只能手动备份/替换 `config.toml`,切换麻烦且容易出错。
### 2.2 项目目标
- **优先做好 Windows 版本**:安装包、路径处理、UI 风格、权限行为均以 Windows 为首要目标平台。
- 提供图形化界面,零 TOML 语法负担地维护供应商与模型别名。
- **支持一键切换当前使用的供应商及其对应模型**:选择供应商后自动筛选关联模型,确认后立即将 `default_model` 写入 `config.toml`。
- **支持多配置文件(Profile)管理**:保存多份独立配置,一键切换激活某一份到 `~/.kimi-code/config.toml`。
- 支持 Kimi Code CLI 官方支持的全部供应商类型。
- 自动处理凭证存储方式(直接字段 vs `[providers.<name>.env]` 子表)。
- 以 Windows MSI 安装包形式分发给 Windows 用户。
## 3. 功能范围
### 3.1 In Scope
- 供应商(providers)管理:增删改查。
- 模型别名(models)管理:增删改查。
- **供应商与模型快速切换**:选择供应商后只显示使用该供应商的模型,一键设为 `default_model`。
- **Profile(配置快照)管理**:新建、重命名、复制、删除、切换 Profile;Profile 保存在 `~/.kimi-code/profiles/` 下,切换时写入 `~/.kimi-code/config.toml`。
- 读取现有 `~/.kimi-code/config.toml`。
- 保存时自动备份原配置并写入新配置。
- 基础校验:必填项、重复别名、缺失的供应商引用、缺失凭证。
- 生成 Windows MSI 安装包。
### 3.2 Out of Scope
- 不实现加密存储:API Key 按 Kimi Code CLI 官方方式保存在 `config.toml` 中。
- 不修改 `tui.toml`、`mcp.json`、OAuth 凭证等其他文件。
- 不实现远程同步或云存储。
- 不实现网络连通性测试或 API 调用验证。
- 第一版不支持 macOS / Linux 安装包,仅输出 Windows x64 MSI;其他平台待 Windows x64 版本完成后再规划。
## 4. 技术方案
### 4.1 推荐方案
**Tauri(Rust + TypeScript)**
- **Tauri v2**:Rust 编写的跨平台桌面框架,内置 Windows MSI 打包,产物体积小、启动快。本设计基于 Tauri v2 的权限与命令体系。
- **Rust**:负责文件 I/O、配置解析、Profile 管理、校验逻辑;类型安全、性能高。
- **TypeScript + React + Tailwind CSS**:负责前端界面;Tailwind 提供基础样式,无需引入重型 UI 组件库。
- **toml_edit**:Rust 生态中支持 round-trip 的 TOML 编辑库,保留注释与格式。
- **IndexMap**:替代 `HashMap` 保留 Provider / Model 的插入顺序。
- **Tauri Bundler**:一键生成 `.msi` 安装包,无需额外 WiX 手动编写。
### 4.2 备选方案
| 方案 | 优点 | 缺点 |
| --- | --- | --- |
| Tauri + Rust + Vanilla TS | 依赖更少,构建更快 | 大型表单状态管理需手写 |
| Tauri + Rust + Vue/Svelte | 体积与 Vanilla 相近 | 团队需熟悉对应框架 |
| egui / Iced(纯 Rust) | 无前端构建链 | UI 灵活度与组件生态不如 Web |
选择 **Tauri + Rust + React** 的原因是:Tauri 内置 MSI 打包且产物体积小;Rust 适合处理文件与配置逻辑;React 表单/表格生态成熟,能快速实现三栏配置界面。
## 5. 目录结构
```
D:/AIGC/KimiSwitch
├── src-tauri/
│ ├── Cargo.toml
│ ├── tauri.conf.json # Tauri 应用配置与打包选项
│ └── src/
│ ├── main.rs # 入口
│ ├── lib.rs # 模块导出
│ ├── models.rs # Provider / Model / Config / Profile 结构体
│ ├── config_io.rs # 读写 config.toml 与 Profile 文件
│ ├── profile_manager.rs # Profile 生命周期管理
│ ├── validators.rs # 配置校验
│ └── commands.rs # Tauri 暴露给前端的命令
├── src/ # 前端源码
│ ├── App.tsx
│ ├── components/
│ │ ├── TopBar.tsx # Profile / 供应商 / 模型 / 设为默认
│ │ ├── ProviderList.tsx
│ │ ├── ProviderForm.tsx
│ │ ├── ModelList.tsx
│ │ ├── ModelForm.tsx
│ │ └── BottomToolbar.tsx
│ ├── hooks/
│ │ └── useConfig.ts # 配置状态与 Tauri 命令封装
│ └── types/
│ └── index.ts # 前端类型定义
├── tests/
│ ├── config_io.rs
│ ├── profile_manager.rs
│ └── validators.rs
├── package.json
├── tsconfig.json
├── vite.config.ts
├── Cargo.lock
├── README.md
└── .gitignore
```
## 6. 数据模型
### 6.1 ProviderType 枚举
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderType {
Kimi,
Anthropic,
Openai,
#[serde(rename = "openai_responses")]
OpenaiResponses,
#[serde(rename = "google-genai")]
GoogleGenai,
Vertexai,
}
```
> 注意:`openai_responses` 与 `google-genai` 包含连字符,无法直接用 `rename_all = "snake_case"`,需单独 `#[serde(rename)]`。
### 6.2 Provider
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Provider {
pub name: String, // 供应商唯一标识
pub provider_type: ProviderType, // 枚举
pub base_url: Option<String>, // 直接字段
pub api_key: Option<String>, // 直接字段
pub env: IndexMap<String, String>, // [providers.<name>.env] 子表,保留顺序
pub raw_other: Table, // 保留本工具不直接编辑的其他字段(如 custom_headers)
}
```
### 6.3 Model
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Model {
pub alias: String, // 模型别名
pub provider: String, // 引用的供应商 name
pub model: String, // 实际模型 ID
pub max_context_size: u64,
pub display_name: Option<String>,
pub raw_other: Table, // 保留 capabilities 等本工具不直接编辑的字段
}
```
### 6.4 Config
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub default_model: Option<String>,
pub providers: IndexMap<String, Provider>, // 保留顺序
pub models: IndexMap<String, Model>, // 保留顺序
pub raw_other: Table, // 保留 config.toml 中与本工具无关的其他字段
}
```
### 6.5 Profile
```rust
#[derive(Debug, Clone)]
pub struct Profile {
pub name: String, // Profile 显示名称,可包含中文/空格
pub filename: String, // 安全化的存储文件名,如 "my-profile.toml"
pub config: Config, // 该 Profile 对应的完整配置内容
pub is_active: bool, // 是否当前已激活
}
```
Profile 文件统一存放在 `~/.kimi-code/profiles/` 目录下。`default` Profile 特殊处理:它直接对应 `~/.kimi-code/config.toml`,不额外在 `profiles/` 下生成文件。
**文件名安全化**:Profile 显示名可包含中文、空格、大小写;存储时自动转译为合法 Windows 文件名。转译规则:保留 ASCII 字母数字,其他字符替换为 `-`,连续 `-` 合并,小写化;若结果为空(如纯中文名),则回退为基于名称哈希的短文件名(如 `profile-48291.toml`)。
## 7. 配置读写行为
### 7.1 读取流程
1. 定位 `~/.kimi-code/config.toml`(可通过 `KIMI_CODE_HOME` 环境变量覆盖;使用 Rust `std::path::PathBuf` 处理 Windows 路径)。
2. 使用 `toml_edit` 解析 TOML(保留注释、空行与字段顺序)。
3. 提取 `[providers.*]` 与 `[models.*]` 到 Rust 结构体。
4. 保留文件中其他字段(如 `[thinking]`、`[loop_control]` 等),保存时原样写回。
### 7.2 写入流程
1. 若目标文件已存在,先复制一份带时间戳的备份:`config.toml.bak.YYYYMMDD_HHMMSS_<毫秒>`(如 1 秒内多次保存,追加自增序号)。
2. 使用 `toml_edit` 加载并修改 `[providers]`、`[models]` 与 `default_model`,保留原文件中的注释、空行与字段顺序。
3. 合并保留的其他字段。
4. 写回文件。
5. 写操作使用临时文件 + 原子重命名(`write to tmp → fsync → rename`),降低写损风险。
> 第一版即使用 `toml_edit` 而非 `toml`/`serde_toml`,避免用户的既有注释和格式在保存后被清空。
### 7.3 Tauri 权限配置
Tauri 默认禁止前端访问文件系统,需在 `tauri.conf.json`(或 `capabilities/` 目录)中显式声明:
- `fs:allow-read-file`:`~/.kimi-code/config.toml`、Profile 文件。
- `fs:allow-write-file`:`~/.kimi-code/config.toml`、Profile 文件。
- `fs:allow-read-dir`:`~/.kimi-code/`、`~/.kimi-code/profiles/`。
- `fs:allow-create-dir`:首次运行时创建 `~/.kimi-code/profiles/`。
- `core:default`:基础窗口与事件权限。
所有路径使用 Tauri 的 `BaseDirectory::Home` 解析,避免硬编码绝对路径。
### 7.4 Tauri 命令清单
Rust 后端通过 `commands.rs` 暴露以下命令给前端调用:
| 命令 | 输入 | 输出 | 说明 |
| --- | --- | --- | --- |
| `load_config` | 无 | `Config` | 读取 `~/.kimi-code/config.toml`,若不存在则初始化空配置。 |
| `save_config` | `Config` | `Result<(), String>` | 校验并写入 `config.toml`,失败返回错误信息。 |
| `list_profiles` | 无 | `Vec<ProfileSummary>` | 列出 `profiles/` 下所有 Profile 及其激活状态。 |
| `load_profile` | `filename: String` | `Config` | 读取指定 Profile 文件内容。 |
| `save_profile` | `filename: String, config: Config` | `Result<(), String>` | 保存或覆盖指定 Profile 文件。 |
| `switch_profile` | `filename: String` | `Result<Config, String>` | 将指定 Profile 内容写入 `config.toml`(带备份)并返回新配置。 |
| `rename_profile` | `old_filename, new_name` | `Result<String, String>` | 重命名 Profile,返回新的安全文件名。 |
| `delete_profile` | `filename: String` | `Result<(), String>` | 删除指定 Profile 文件。 |
| `open_config_dir` | 无 | `Result<(), String>` | 调用系统文件管理器打开配置目录。 |
| `get_app_version` | 无 | `String` | 返回当前应用版本号。 |
### 7.5 凭证存储策略
根据 [Kimi Code CLI 配置覆盖文档](https://www.kimi.com/code/docs/kimi-code-cli/configuration/overrides.html),凭证解析优先级为:
1. `[providers.<name>].api_key` / `.base_url`
2. `[providers.<name>.env]` 中对应键
3. 两者都缺失 → CLI 启动报错
KimiSwitch 默认将凭证写入 `[providers.<name>.env]` 子表,使用官方约定键名,同时提供「使用直接字段」开关,允许用户将 `api_key` / `base_url` 写到 `[providers.<name>]` 顶层。UI 中两种方式互斥:
- **env 子表模式**:`api_key` / `base_url` 输入框的值映射到 `env` 中的约定键名;保存时顶层 `api_key` / `base_url` 字段被移除。
- **直接字段模式**:`api_key` / `base_url` 写入顶层字段;保存时 `env` 中的对应约定键名被移除,其他自定义 `env` 键保留。
避免直接字段与 `env` 同时存在导致优先级迷惑。
各供应商的默认 Base URL 与 `env` 键名如下(参考 [Kimi Code CLI 平台与模型](https://www.kimi.com/code/docs/kimi-code-cli/configuration/providers.html)):
| 供应商类型 | 默认 Base URL | API Key 键名 | Base URL 键名 | 特殊说明 |
| --- | --- | --- | --- | --- |
| `kimi` | `https://api.moonshot.ai/v1` | `KIMI_API_KEY` | `KIMI_BASE_URL` | 支持视频上传 |
| `anthropic` | Anthropic SDK 默认 | `ANTHROPIC_API_KEY` | `ANTHROPIC_BASE_URL` | Claude 模型自动识别 thinking / 视觉 / 工具调用 |
| `openai` | `https://api.openai.com/v1` | `OPENAI_API_KEY` | `OPENAI_BASE_URL` | 兼容 DeepSeek、Qwen 等第三方网关 |
| `openai_responses` | `https://api.openai.com/v1` | `OPENAI_API_KEY` | `OPENAI_BASE_URL` | 使用 OpenAI Responses API |
| `google-genai` | `https://generativelanguage.googleapis.com` | `GOOGLE_API_KEY` | `GOOGLE_GEMINI_BASE_URL` | 只填主机根地址,不要带 `/v1beta` |
| `vertexai` | 区域化 `*-aiplatform.googleapis.com` | 通常不需要(走 ADC) | `GOOGLE_VERTEX_BASE_URL` | 必须提供 `GOOGLE_CLOUD_PROJECT` 与 `GOOGLE_CLOUD_LOCATION`;只填主机根地址 |
创建供应商时,UI 根据所选 `type` 自动填入默认 Base URL 与推荐 `env` 键名,用户可手动覆盖。
### 7.6 Profile 生命周期
Profile 是 `~/.kimi-code/config.toml` 的命名快照,存放在 `~/.kimi-code/profiles/` 目录下。
#### 7.6.1 初始化
- 工具首次启动时,若 `~/.kimi-code/config.toml` 已存在,自动生成一个名为 `default` 的 Profile,其内容即为当前 `config.toml`。
- 若 `config.toml` 不存在,创建一个空 `default` Profile。
- `default` Profile 始终直接对应 `~/.kimi-code/config.toml`,不在 `profiles/` 下额外生成文件。
**文件名安全化**:Profile 显示名可包含中文、空格、大小写;存储时自动转译为合法 Windows 文件名。转译规则:保留 ASCII 字母数字,其他字符替换为 `-`,连续 `-` 合并,小写化;若结果为空(如纯中文名),则回退为基于名称哈希的短文件名(如 `profile-48291.toml`)。
#### 7.6.2 操作
| 操作 | 行为 |
| --- | --- |
| 新建 Profile | 用户输入显示名,自动生成安全文件名;可选择「从当前配置复制」或「空白」;保存到 `profiles/<safe-name>.toml`。 |
| 重命名 Profile | 仅重命名非 `default` 的 Profile,修改显示名并重新生成安全文件名;原文件删除,新文件写入。 |
| 复制 Profile | 以 `<原显示名> Copy` 创建副本,自动生成新的安全文件名。 |
| 删除 Profile | 删除 `profiles/<safe-name>.toml`;`default` 不可删除。 |
| 切换 Profile | 将选中的 Profile 内容写入 `~/.kimi-code/config.toml`(先备份当前 `config.toml`),然后重新加载界面。 |
| 保存当前到 Profile | 把当前编辑内容导出为新的 Profile 文件,不影响当前激活状态。 |
#### 7.6.3 激活状态判定
- 工具启动时将 `config.toml` 解析为 `Config`,再与各 Profile 的 `Config` 做深度比较(而非字符串比较),语义一致即标记为 `is_active`。
- 若没有任何 Profile 与当前 `config.toml` 语义一致,则 `default` Profile 标记为 `is_active`,并在界面上提示「当前 config.toml 与所有 Profile 不完全匹配」。
#### 7.6.4 未保存变更处理
- 切换 Profile 或退出前若存在未保存的编辑,弹出对话框:「保存当前修改 / 丢弃 / 取消」。
- 标题栏或状态栏显示「已修改」指示器。
## 8. 界面设计
前端使用 React + TypeScript + Tailwind CSS 实现,主窗口采用三栏布局,顶部增加「快速切换」工具条。前端通过 Tauri `invoke` 调用 Rust 命令完成所有文件操作。
- **窗口约束**:最小宽度 1000px,最小高度 700px;标题栏显示 `[未保存] Profile 名 - KimiSwitch`。
- **快捷键**:`Ctrl + S` 保存当前配置,`Ctrl + R` 重新读取,`Ctrl + O` 打开配置目录。第一版不单独做主题切换,跟随系统亮/暗模式。
### 8.1 顶部工具条
从左到右依次为:
- **Profile 下拉框**:列出所有 Profile(含 `default`),当前激活项高亮显示。
- **Profile 操作按钮**:「新建」「复制」「重命名」「删除」「切换」。
- **当前供应商** 下拉框:列出当前 Profile 中已配置供应商。
- **当前模型** 下拉框:根据所选供应商,只列出引用该供应商的模型别名;若该供应商下没有模型,则提示「未绑定模型,请先在右侧添加」。
- **设为默认** 按钮:将当前选中的模型写入 `default_model` 字段,并立即保存到当前 Profile 与 `config.toml`。
- 启动时自动高亮当前 `default_model` 对应的供应商和模型。
### 8.2 左栏:供应商列表
- 显示所有 provider name 与 type。
- 支持「添加」与「删除」按钮。
- 切换左栏选中项时,顶部「当前供应商」下拉框同步联动,右栏模型列表按该供应商过滤。
### 8.3 中栏:供应商详情表单
- `type` 下拉选择框;切换时自动填充默认 `base_url` 与推荐 `env` 键名。
- `base_url` 输入框。
- `api_key` 输入框(默认掩码,可切换明文)。
- 凭证存储方式单选:直接字段 / env 子表;两种方式互斥,保存时只写入被选中的那一种。
- `env` 键值对表格(可增删改行)。
### 8.4 右栏:模型别名列表与详情
- 显示当前选中供应商下的 model alias(也可切换显示全部)。
- 表单包含 `alias`、`provider` 下拉选择、`model`、`max_context_size`、`display_name`。
- 提供「设为默认模型」按钮,等同于顶部快速切换区的「设为默认」。
### 8.5 状态与错误处理
- **未保存指示**:标题栏在配置有未保存变更时显示 `*` 前缀;状态栏显示「已修改」或「已保存」。
- **切换/退出前检查**:存在未保存变更时,弹出「保存 / 丢弃 / 取消」三选一对话框。
- **文件不存在**:首次启动或配置目录不存在时,自动创建 `~/.kimi-code/` 目录与空 `config.toml`,不报错。
- **文件只读/权限不足**:保存失败时给出明确提示,并建议检查目录权限或以管理员身份运行。
- **校验失败**:保存前校验不通过时,弹窗列出所有错误项,禁止写入。
- **备份失败**:若备份步骤失败(如磁盘满),禁止覆盖原文件。
- **多实例冲突**:启动时检测 `~/.kimi-code/.kimiswitch.lock`;若已存在且进程仍在运行,提示用户「KimiSwitch 已在运行,请勿多开」;若进程已消失则清理旧锁。保存时不强制全局锁,但建议单次会话内顺序执行写操作。
### 8.6 底部工具栏
- **读取配置**:从 `~/.kimi-code/config.toml` 重新加载。
- **保存配置**:校验并写入当前 Profile 与 `config.toml`,失败时弹出错误提示。
- **保存为新 Profile**:把当前编辑内容导出为新的 Profile 文件。
- **打开配置目录**:调用系统文件管理器定位到数据目录。
## 9. 校验规则
保存前必须校验通过,否则禁止写入:
1. 每个 Provider 的 `name` 非空、不重复,且为合法 TOML 表键(必要时生成带引号的键)。
2. 每个 Provider 的 `type` 必须是支持的类型之一。
3. 每个 Provider 必须至少提供一种凭证来源,但 `vertexai` 例外(走 Google Cloud ADC,无需静态 API Key):
- `api_key` 直接字段非空,或
- `env` 中包含对应供应商的 API Key 键名。
4. 凭证存储方式在直接字段与 `env` 子表之间互斥,保存时只写入选中的那一种。
5. `vertexai` 类型的 Provider 必须在 `env` 中提供 `GOOGLE_CLOUD_PROJECT` 与 `GOOGLE_CLOUD_LOCATION`,不强制要求 API Key。
6. 每个 Model 的 `alias` 非空、不重复,且为合法 TOML 表键(含 `.` 等字符时自动加引号)。
7. 每个 Model 的 `provider` 必须引用一个已存在的 Provider。
8. 每个 Model 的 `model` 非空,`max_context_size` 为正整数。
## 10. MSI 打包
### 10.1 打包流程
1. 安装依赖:`npm install` 与 `cargo fetch`
2. 开发/构建前端:`npm run build`
3. 构建并打包:`npm run tauri build`
- Tauri 自动调用 `cargo build --release` 与 Windows bundler
- 输出:`src-tauri/target/release/bundle/msi/KimiSwitch_<version>_x64_en-US.msi`
### 10.2 MSI 行为
- 默认安装目录:`C:\Program Files\KimiSwitch`( per-machine 安装,需管理员权限)。
- 创建开始菜单快捷方式:`KimiSwitch`。
- 安装程序不自动写入 `config.toml`,仅部署工具本身。
- 支持标准卸载。
- 可在 `tauri.conf.json` 中配置签名、版本、厂商信息、安装目录等。
- 第一版仅构建 x64 架构 MSI,不构建 arm64 或 32 位版本。
- 第一版**不启用 Tauri Updater**,后续版本通过重新安装 MSI 升级,降低第一版复杂度。
### 10.3 Windows 专项处理
- **路径解析**:使用 Rust `dirs::home_dir()` 获取用户主目录,拼接 `.kimi-code`,避免依赖 `$HOME` 环境变量(Windows 下可能不存在)。
- **配置文件位置**:默认 `C:\Users\<用户名>\.kimi-code\config.toml`;若用户设置 `KIMI_CODE_HOME`,按该变量解析。
- **权限**:工具运行时只读写用户目录下的文件,无需以管理员身份运行;仅 MSI 安装本身需要管理员权限。
- **编码与换行**:TOML 文件统一使用 UTF-8;Windows 下换行保持为 `\r\n` 或 `\n` 均可,`toml_edit` 会保留原文件换行风格。
- **代码签名(可选)**:如后续分发,建议为 MSI 配置代码签名证书,减少 Windows Defender SmartScreen 提示。
## 11. 测试策略
- **单元测试**(Rust):
- `config_io.rs`:解析与生成 TOML 的 round-trip 测试。
- `profile_manager.rs`:Profile 新建、复制、切换、激活状态判定测试。
- `validators.rs`:校验规则的边界测试。
- **手动测试**:
- 启动 GUI,增删改供应商与模型。
- 创建多个 Profile 并相互切换,验证 `config.toml` 内容正确变化。
- 保存后使用 Kimi Code CLI 读取并验证可用性。
- 在 Windows 上执行 MSI 安装与卸载。
## 12. 假设与约束
- **第一版目标平台仅为 Windows x64**,macOS / Linux 跨平台支持不在第一版范围内,待 Windows x64 版本完成后再规划。
- API Key 按 Kimi Code CLI 官方方式明文存储在 `config.toml` 中,不引入额外加密。
- 开发者已安装 Rust 工具链、Node.js 与 Tauri CLI(仅开发与打包时需要,最终用户不需要)。
- 不覆盖 Kimi Code CLI 的自动更新机制或 OAuth 登录流程。
- 最终用户运行 KimiSwitch 只需标准 Windows 用户权限,安装 MSI 时才需要管理员权限。
## 13. 参考文档
- [Kimi Code CLI 配置文件](https://www.kimi.com/code/docs/kimi-code-cli/configuration/config-files.html)
- [Kimi Code CLI 平台与模型](https://www.kimi.com/code/docs/kimi-code-cli/configuration/providers.html)
- [Kimi Code CLI 数据路径](https://www.kimi.com/code/docs/kimi-code-cli/configuration/data-locations.html)
- [Kimi Code CLI 环境变量](https://www.kimi.com/code/docs/kimi-code-cli/configuration/env-vars.html)
- [Kimi Code CLI 配置覆盖](https://www.kimi.com/code/docs/kimi-code-cli/configuration/overrides.html)
@@ -1,47 +0,0 @@
# Kimi Code 选项卡 /reload 提示设计
## 背景
用户在 Kimi Code 选项卡下切换供应商后,Kimi Code CLI 需要执行 `/reload` 才能加载新的提供方配置。当前界面没有给出该提示,用户容易遗漏这一步。
## 目标
在 Kimi Code 选项卡下,给每个供应商的"切换使用"按钮旁边增加一个信息提示,告知用户切换供应商后需要执行 `/reload`。
## 设计
### 位置与交互
- 在 `ProviderList` 组件中,每个供应商卡片右侧的"切换使用"按钮旁边添加一个信息图标 `ⓘ`。
- 使用原生 `title` 属性展示 tooltip,鼠标悬停时显示提示文案。
- 仅在当前选中 `agent === "kimi_code"` 时显示该图标;Pi 选项卡下不显示。
### 文案
- 中文:`切换供应商后,请在 Kimi Code 中执行 /reload 以生效。`
- 英文:`After switching providers, run /reload in Kimi Code to apply the change.`
### 样式
- 图标大小约 14px,颜色为 `text-gray-500`,hover 时变为 `text-gray-300`。
- 图标与"切换使用"按钮间距约 8px(`gap-2`)。
- 不引入额外依赖或自定义 tooltip 组件。
### 组件与数据流
- `src/components/ProviderList.tsx`:
- `ProviderListProps` 新增 `agent: Agent` 字段。
- 根据 `agent` 决定是否渲染提示图标。
- 图标 `title` 使用 `t("switchReloadHint")`。
- `src/App.tsx`:
- 渲染 `<ProviderList />` 时传入 `agent={agent}`。
- `src/i18n/zh.ts` 与 `src/i18n/en.ts`:
- 新增键 `switchReloadHint`。
## 验收标准
- [ ] 切换到 Kimi Code 选项卡时,每个供应商的"切换使用"按钮旁出现 `ⓘ` 图标。
- [ ] 鼠标悬停在图标上时,显示正确语言版本的 `/reload` 提示。
- [ ] 切换到 Pi 选项卡时,该图标不显示。
- [ ] 不引入新的运行时依赖。
- [ ] TypeScript 编译与项目构建保持通过。
@@ -1,253 +0,0 @@
# Kimi Code 全局配置面板设计
## 背景
Kimi Code CLI 的 `~/.kimi-code/config.toml` 包含大量全局设置:`[thinking]`、`[loop_control]`、`[background]`、`[[permission.rules]]`、`[[hooks]]` 等。当前 Kimi Switch 只暴露了 `providers` 和 `models` 的可视化编辑,其他全局配置只能通过"配置 JSON"标签页手动修改,不够便捷。
## 目标
1. 在 Kimi Code 选项卡的供应商编辑页面(`ProviderEdit`)中,"模型映射"标签页下方新增一个"全局配置"面板,用图形化控件快速编辑上述 Kimi Code 全局设置。
2. 根据模型 ID 自动为新添加/发现的模型设置默认 `max_context_size`,全局默认值为 256000,具体模型按可扩展的键值对表格匹配。
模型级的 `max_context_size` 已在模型映射表格中实现,用户仍可手动覆盖自动默认值。
## 设计
### 位置
- 组件:`src/components/ProviderEdit.tsx`
- 仅在 `agent === "kimi_code"` 时显示。
- 位于"模型映射"标签页的内容区域内,在模型映射表格和"+ 添加模型映射"按钮的下方。
- 垂直排列,超出区域高度时由页面滚动条处理。
### 分组与控件
面板按功能分为 5 张卡片:
#### 卡片 1:思考模式 `[thinking]`
| 控件 | 字段 | 说明 |
| --- | --- | --- |
| 复选框 | `enabled` | 是否默认开启思考 |
| 分段按钮 | `effort` | 思考强度:`low` / `medium` / `high` / `max` |
| 复选框 | `keep` | 是否保留历史思考内容(`"all"` 或关值) |
- 思考等级和保留思考内容仅在"启用思考"勾选时可用,禁用时置灰。
- "保留思考内容"未勾选时,写入 `[thinking].keep = false`(不能省略,否则 CLI 会回退到默认值 `"all"`)。
- 卡片底部小字提示:"启用思考会占用更多上下文,请确保模型上下文长度和预留空间足够。"
#### 卡片 2:循环控制 `[loop_control]`
| 控件 | 字段 | 说明 |
| --- | --- | --- |
| 数字输入框 | `max_retries_per_step` | 单步失败后最大重试次数,默认 3 |
| 数字输入框 | `reserved_context_size` | 预留给模型输出的 token 数,单位 token |
#### 卡片 3:后台任务 `[background]`
| 控件 | 字段 | 说明 |
| --- | --- | --- |
| 数字输入框 | `max_running_tasks` | 同时运行的最大后台任务数 |
| 复选框 | `keep_alive_on_exit` | 会话关闭时是否保留后台任务 |
#### 卡片 4:权限规则 `[[permission.rules]]`
- 可添加/删除的规则列表,默认空列表。
- 每行包含:
- **处置** 下拉框:`allow` / `deny` / `ask`
- **模式** 输入框:如 `Read`、`Bash(rm -rf*)`
- **删除** 按钮
- 提供"添加常用规则"快捷入口,可一键插入:
- `allow` + `Read`
- `deny` + `Bash(rm -rf*)`
#### 卡片 5:生命周期钩子 `[[hooks]]`
- 可添加/删除的钩子列表,默认空列表。
- 每行包含:
- **事件** 下拉框:常见事件如 `PreToolUse` / `PostToolUse`,也可手动输入其他事件
- **匹配器** 输入框:如 `Bash`
- **命令** 输入框:如 `node ~/.kimi-code/hooks/check-bash.mjs`
- **超时** 数字输入框(秒)
- **删除** 按钮
### 样式
- 卡片容器:`bg-[#16161a] border border-[#2a2a2e] rounded-xl p-4`
- 卡片间距:`gap-4`
- 卡片标题:`text-gray-400 text-sm font-medium mb-3`
- 表单项:`flex items-center gap-4 flex-wrap`
- 数字输入框宽度:`w-32`
- 禁用状态:文字 `text-gray-600`,按钮不可交互
### 字段默认值
当 `raw_other` 中不存在对应表时,控件按以下默认值展示:
| 配置块 | 字段 | UI 默认值 |
| --- | --- | --- |
| `[thinking]` | `enabled` | `true` |
| `[thinking]` | `effort` | `"medium"` |
| `[thinking]` | `keep` | `"all"`(勾选) |
| `[loop_control]` | `max_retries_per_step` | `3` |
| `[loop_control]` | `reserved_context_size` | `50000` |
| `[background]` | `max_running_tasks` | 空(不写入) |
| `[background]` | `keep_alive_on_exit` | `false` |
| `[[permission.rules]]` | — | 空列表 |
| `[[hooks]]` | — | 空列表 |
数字输入框允许清空,表示不写入配置文件,由 Kimi Code CLI 使用其内置默认值。
### 数据流
- `App.tsx` 向 `ProviderEdit` 传入:
- `agent`
- `rawOther`(即 `config.raw_other`)
- `updateRawOther: (updater: (rawOther: unknown) => unknown) => void`
- `ProviderEdit` 中新增内部子组件 `AgentSettingsPanel`,封装所有全局配置 UI。
- 新增 TypeScript 类型(`src/types/index.ts`):
- `ThinkingConfig`
- `LoopControlConfig`
- `BackgroundConfig`
- `PermissionRule`
- `Hook`
- `AgentSettings`
- 新增 helper(`src/components/AgentSettingsPanel.tsx` 或 `src/lib/agent-settings.ts`):
- `getAgentSettings(rawOther: unknown): AgentSettings`
- `setAgentSettings(rawOther: unknown, patch: Partial<AgentSettings>): unknown`
- 统一从 `raw_other` 中读取/写入 `[thinking]`、`[loop_control]`、`[background]`、`[[permission.rules]]`、`[[hooks]]`。
- 后端无需改动:这些顶层表原本就通过 `Config.raw_other` 在导入/导出时保留。
### 模型默认上下文长度
在新增模型(手动添加、一键设置、从 API 发现模型)时,根据模型 ID 自动推荐 `max_context_size`。
- 默认回退值:`256000`
- 匹配方式:按模型 ID 字符串进行不区分大小写的正则前缀/全名匹配(代码中使用 `/pattern/i`),命中第一条规则即返回。
- 规则表(按匹配优先级排列):
| 匹配规则(不区分大小写) | `max_context_size` | 说明 |
| --- | --- | --- |
| **国内模型(优先)** | | |
| `^kimi-for-coding$` | `262144` | Kimi Code 官方托管模型 |
| `^kimi-k2\.5` | `256000` | Kimi K2.5 系列 |
| `^kimi-k2` | `256000` | Kimi K2 系列 |
| `^kimi-` | `256000` | 其他 Kimi 模型 |
| `^glm-5\.2` | `1000000` | GLM-5.2 |
| `^glm-5\.1` | `256000` | GLM-5.1 |
| `^glm-5` | `256000` | GLM-5 系列 |
| `^glm-4` | `128000` | GLM-4 / GLM-4-Plus / GLM-4-Flash |
| `^glm-` | `128000` | 其他 GLM 模型 |
| `^MiniMax-M3` | `1000000` | MiniMax-M3 |
| `^MiniMax-Text-01` | `400000` | MiniMax-Text-01 |
| `^MiniMax-` | `256000` | 其他 MiniMax 模型 |
| `^qwen2\.5` | `128000` | 通义千问 Qwen2.5 系列 |
| `^qwen-max` | `128000` | 通义千问 Max |
| `^qwen-plus` | `128000` | 通义千问 Plus |
| `^qwen-turbo` | `128000` | 通义千问 Turbo |
| `^qwen-coder` | `128000` | 通义千问 Coder |
| `^qwen-` | `128000` | 其他通义千问模型 |
| `^deepseek-r1` | `64000` | DeepSeek-R1 |
| `^deepseek-v3` | `64000` | DeepSeek-V3 |
| `^deepseek-coder` | `64000` | DeepSeek-Coder |
| `^deepseek-` | `64000` | 其他 DeepSeek 模型 |
| `^hunyuan-pro` | `32000` | 腾讯 Hunyuan Pro |
| `^hunyuan-standard` | `32000` | 腾讯 Hunyuan Standard |
| `^hunyuan-lite` | `32000` | 腾讯 Hunyuan Lite |
| `^hunyuan-` | `32000` | 其他 Hunyuan 模型 |
| `^doubao-pro` | `128000` | 字节 Doubao Pro |
| `^doubao-lite` | `128000` | 字节 Doubao Lite |
| `^doubao-vision` | `128000` | 字节 Doubao Vision |
| `^doubao-` | `128000` | 其他 Doubao 模型 |
| `^ernie-4\.0` | `128000` | 百度文心 4.0 |
| `^ernie-3\.5` | `128000` | 百度文心 3.5 |
| `^ernie-speed` | `128000` | 百度文心 Speed |
| `^ernie-lite` | `128000` | 百度文心 Lite |
| `^ernie-` | `128000` | 其他文心模型 |
| `^spark-v4` | `32000` | 讯飞星火 V4 |
| `^spark-v3\.5` | `32000` | 讯飞星火 V3.5 |
| `^spark-pro` | `32000` | 讯飞星火 Pro |
| `^spark-max` | `32000` | 讯飞星火 Max |
| `^spark-` | `32000` | 其他星火模型 |
| `^sensechat-` | `128000` | 商汤 SenseChat |
| `^baichuan-4` | `128000` | 百川 Baichuan 4 |
| `^baichuan-3` | `128000` | 百川 Baichuan 3 |
| `^baichuan-` | `128000` | 其他百川模型 |
| `^yi-` | `128000` | 零一万物 Yi 系列 |
| **国际模型** | | |
| `^claude-opus` | `200000` | Claude Opus 系列(多数版本为 200K;Claude 4 部分版本可达 1M,这里取保守值) |
| `^claude-sonnet` | `200000` | Claude Sonnet 系列 |
| `^claude-haiku` | `200000` | Claude Haiku 系列 |
| `^claude-` | `200000` | 其他 Claude 模型 |
| `^gpt-4\.1` | `1047576` | GPT-4.1 系列(1M tokens) |
| `^gpt-4o` | `128000` | GPT-4o 系列 |
| `^gpt-4-turbo` | `128000` | GPT-4 Turbo |
| `^gpt-4-` | `128000` | 其他 GPT-4 模型 |
| `^gemini-2\.0-flash` | `1048576` | Gemini 2.0 Flash(1M tokens) |
| `^gemini-1\.5-pro` | `2097152` | Gemini 1.5 Pro(2M tokens) |
| `^gemini-1\.5-flash` | `1048576` | Gemini 1.5 Flash(1M tokens) |
| `^gemini-` | `1048576` | 其他 Gemini 模型 |
| (默认) | `256000` | 未命中任何规则 |
> 注:模型上下文长度取公开资料典型值,部分国内/国际模型存在多个版本导致数值差异,实际以供应商文档为准。规则表以代码常量形式存在,按 specificity 从高到低排列,命中第一条即返回,未命中时回退到 256000。用户仍可在模型映射表格中手动覆盖。
- 实现位置:`src/lib/model-defaults.ts`(或类似纯函数文件)。
- 核心函数:
```ts
export function getDefaultMaxContextSize(modelId: string): number;
```
- 调用点:
- `ProviderEdit` 中手动添加模型时。
- `ProviderEdit` 中通过"获取模型列表"发现模型并添加时。
- `handleAddProvider` / `handleApplyProviderJson` 等生成默认模型的地方,可一并使用此函数作为更智能的默认值。
### i18n 键
新增键:
- `agentSettings`: "全局配置"
- `enableThinking`: "启用思考"
- `thinkingLevel`: "思考等级"
- `thinkingKeep`: "保留思考内容"
- `thinkingLow`: "低"
- `thinkingMedium`: "中"
- `thinkingHigh`: "高"
- `thinkingMax`: "最大"
- `thinkingContextHint`: "启用思考会占用更多上下文,请确保模型上下文长度和预留空间足够。"
- `loopControlSettings`: "循环控制"
- `maxRetriesPerStep`: "单步重试次数"
- `reservedContextSize`: "上下文预留大小"
- `backgroundSettings`: "后台任务"
- `maxRunningTasks`: "最大并发数"
- `keepAliveOnExit`: "退出时保持运行"
- `permissionRules`: "权限规则"
- `permissionDecision`: "处置"
- `permissionPattern`: "模式"
- `permissionAllow`: "允许"
- `permissionDeny`: "拒绝"
- `permissionAsk`: "询问"
- `addRule`: "+ 添加规则"
- `addCommonRules`: "添加常用规则"
- `hooks`: "生命周期钩子"
- `hookEvent`: "事件"
- `hookMatcher`: "匹配器"
- `hookCommand`: "命令"
- `hookTimeout`: "超时"
- `addHook`: "+ 添加钩子"
## 验收标准
- [ ] 在 Kimi Code 选项卡下进入供应商编辑页面,"模型映射"标签页下方出现"全局配置"区域。
- [ ] Pi 选项卡下不显示该全局配置区域。
- [ ] "启用思考"复选框控制 `[thinking].enabled`。
- [ ] 启用思考后,"思考等级"和"保留思考内容"可用;未启用时禁用。
- [ ] "思考等级"分段按钮写入 `[thinking].effort`。
- [ ] "保留思考内容"写入 `[thinking].keep = "all"`,未勾选时写入关值。
- [ ] `[loop_control]` 和 `[background]` 的数字/复选框正确读写。
- [ ] 权限规则列表可增删改,导出为 `[[permission.rules]]`。
- [ ] 钩子列表可增删改,导出为 `[[hooks]]`。
- [ ] 保存配置后,Kimi Code 的 `config.toml` 中对应顶层表内容正确。
- [ ] 不修改模型映射表格中已有的 `max_context_size` 手动编辑逻辑。
- [ ] 添加新模型时,`max_context_size` 默认按规则表自动填充,匹配不区分大小写,未命中规则时为 256000。
- [ ] 用户仍可在模型映射表格中手动覆盖自动填充的 `max_context_size`。
- [ ] `npm run build` 通过。