diff --git a/README.md b/README.md
index 4f36ec7..eca90ff 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Pi Switch
+# Kimi Switch
> Windows 桌面端 LLM 供应商配置管理器,让你在 **Kimi Code CLI** 和 **Pi** 两个 Agent 之间无缝切换多家 LLM 供应商与模型。
@@ -32,12 +32,12 @@
## 这是什么
-**Pi Switch** 是一个为 LLM CLI 用户打造的 Windows 桌面配置管理工具。它解决两个痛点:
+**Kimi Switch** 是一个为 LLM CLI 用户打造的 Windows 桌面配置管理工具。它解决两个痛点:
1. **多供应商管理繁琐**:在 Kimi / Anthropic / OpenAI / Google GenAI / 自建代理 等多家供应商之间切换时,需要反复手改 TOML/JSON,容易出错。
2. **多 Agent 配置割裂**:同时使用 **Kimi Code CLI** 和 **Pi** 两个 Agent 的开发者,每个 Agent 都有一套独立的配置格式,改一处要改两份。
-Pi Switch 提供统一的图形界面:
+Kimi Switch 提供统一的图形界面:
- 统一图形界面,分别管理 Kimi Code 和 Pi 两套 Agent 配置,顶部 Tab 切换
- 一键切换供应商,自动写入对应 Agent 的原生配置
@@ -66,7 +66,7 @@ Pi Switch 提供统一的图形界面:
```
┌────────────────────────────────────────────────────────────────────┐
-│ Pi Switch (Tauri v2) │
+│ Kimi Switch (Tauri v2) │
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ React Frontend (TS) │ │ Rust Backend (lib.rs) │ │
@@ -86,17 +86,17 @@ Pi Switch 提供统一的图形界面:
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ SQLite │ │ Agent 原生配置文件 │
- │ ~/.pi-switch/ │ │ ├─ ~/.kimi-code/ │
- │ pi-switch.db │ │ │ └─ config.toml │
+ │ ~/.kimi-switch/ │ │ ├─ ~/.kimi-code/ │
+ │ kimi-switch.db │ │ │ └─ config.toml │
│ │ │ └─ ~/.pi/agent/ │
- │ (Pi Switch 内部状态) │ │ ├─ models.json │
+ │ (Kimi Switch 内部状态) │ │ ├─ models.json │
└──────────────────────┘ │ └─ settings.json │
└──────────────────────────┘
```
**关键设计**:
-- Pi Switch 维护自己的 SQLite 数据库存**完整配置**(所有供应商 + 所有模型,含未激活的)
+- Kimi Switch 维护自己的 SQLite 数据库存**完整配置**(所有供应商 + 所有模型,含未激活的)
- 点「切换使用」时只把**当前激活的供应商**写入 Agent 原生配置,其他不写
- 这样 Kimi Code / Pi 始终只看到一个活跃供应商,不会被托管/OAuth 默认值干扰
- `raw_other` 字段透传未知键,保证前后往返不丢字段
@@ -118,11 +118,11 @@ Pi Switch 提供统一的图形界面:
| 文件 | 用途 | 备份 |
| --- | --- | --- |
-| `%USERPROFILE%\.pi-switch\pi-switch.db` | Pi Switch 自己的 SQLite 数据库,存全量配置 | — |
+| `%USERPROFILE%\.kimi-switch\kimi-switch.db` | Kimi Switch 自己的 SQLite 数据库,存全量配置 | — |
| `%USERPROFILE%\.kimi-code\config.toml` | Kimi Code CLI 的 TOML 配置(**切换时写入**) | 同目录下 `backups/config.toml.bak.{YYYYMMDD_HHMMSS}`,保留 7 天 |
| `%USERPROFILE%\.pi\agent\models.json` | Pi 的供应商+模型配置(**切换时写入**) | 同目录下 `backups/models.json.bak.{YYYYMMDD_HHMMSS}`,保留 7 天 |
| `%USERPROFILE%\.pi\agent\settings.json` | Pi 的默认供应商/模型(**切换时写入**) | 同目录下 `backups/settings.json.bak.{YYYYMMDD_HHMMSS}`,保留 7 天 |
-| `localStorage[pi-switch-agent]` | 前端记住上次选中的 Agent(kimi_code / pi) | — |
+| `localStorage[kimi-switch-agent]` | 前端记住上次选中的 Agent(kimi_code / pi) | — |
环境变量覆盖:
@@ -205,7 +205,7 @@ npm run dev
│ └── tauri.conf.json # Tauri 配置(窗口/打包/CSP)
│
├── scripts/generate-icons.py # 从 SVG 生成各尺寸图标
-├── public/pi.svg # 应用图标源(蓝紫渐变 π)
+├── public/kimi.svg # 应用图标源(蓝紫渐变 π)
└── docs/superpowers/ # 设计规范与实施计划
```
@@ -283,7 +283,7 @@ npm run tauri-build
产物位置:
-- `src-tauri/target/release/bundle/msi/Pi Switch_0.1.0_x64_en-US.msi`
+- `src-tauri/target/release/bundle/msi/Kimi Switch_0.1.0_x64_en-US.msi`
Windows 安装包(MSI),含 WebView2 bootstrapper 自动下载。`nsis` 已禁用,目前只产 MSI。
@@ -303,7 +303,7 @@ A: 在 Kimi Code 会话里执行 `/reload`(Kimi Code CLI 才会重新读取 `~
A: 关闭窗口前会弹原生 `beforeunload` 提示,标题栏也会显示 `*` 前缀。
**Q: 怎么备份/迁移我的配置?**
-A: 备份 `%USERPROFILE%\.pi-switch\pi-switch.db` 即可,里面存了全量配置(含未激活的供应商)。
+A: 备份 `%USERPROFILE%\.kimi-switch\kimi-switch.db` 即可,里面存了全量配置(含未激活的供应商)。
**Q: Vertex AI 为什么不能拉取模型列表?**
A: Vertex 需要 GCP project/location 凭证,当前实现留了 TODO,等 GCP SDK 集成后再补。
@@ -314,7 +314,7 @@ A: 代码不依赖 Windows 专属 API,但 `tauri.conf.json` 的 bundle 目标
## 安全提示
- API Key 以明文存储在本地 SQLite 和 Agent 原生配置里——**不要在共享电脑上保存**
-- 不要把 `pi-switch.db`、`config.toml`、`models.json` 提交到 Git
+- 不要把 `kimi-switch.db`、`config.toml`、`models.json` 提交到 Git
- 应用 CSP 已收紧(`default-src 'self'`),但 WebView2 仍可能缓存表单内容,注意在公共电脑用完退出
---
diff --git a/README_EN.md b/README_EN.md
index 2a1336f..8751aef 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -1,4 +1,4 @@
-# Pi Switch
+# Kimi Switch
> A Windows desktop LLM provider config manager that lets you switch between multiple LLM providers and models across the **Kimi Code CLI** and **Pi** agents.
@@ -32,12 +32,12 @@
## What Is This
-**Pi Switch** is a Windows desktop configuration tool built for LLM CLI users. It solves two pain points:
+**Kimi Switch** is a Windows desktop configuration tool built for LLM CLI users. It solves two pain points:
1. **Tedious multi-provider management**: switching between Kimi / Anthropic / OpenAI / Google GenAI / self-hosted proxies requires repeatedly hand-editing TOML/JSON, which is error-prone.
2. **Fragmented multi-agent configs**: developers using both **Kimi Code CLI** and **Pi** have to maintain two independent config formats, duplicating every change.
-Pi Switch provides a unified GUI:
+Kimi Switch provides a unified GUI:
- One unified interface to manage two separate agent configs (Kimi Code and Pi), switched via a top tab
- One-click provider switching that writes the active provider into the target agent's native config
@@ -66,7 +66,7 @@ Pi Switch provides a unified GUI:
```
┌────────────────────────────────────────────────────────────────────┐
-│ Pi Switch (Tauri v2) │
+│ Kimi Switch (Tauri v2) │
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ React Frontend (TS) │ │ Rust Backend (lib.rs) │ │
@@ -86,17 +86,17 @@ Pi Switch provides a unified GUI:
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ SQLite │ │ Agent native configs │
- │ ~/.pi-switch/ │ │ ├─ ~/.kimi-code/ │
- │ pi-switch.db │ │ │ └─ config.toml │
+ │ ~/.kimi-switch/ │ │ ├─ ~/.kimi-code/ │
+ │ kimi-switch.db │ │ │ └─ config.toml │
│ │ │ └─ ~/.pi/agent/ │
- │ (Pi Switch internal)│ │ ├─ models.json │
+ │ (Kimi Switch internal)│ │ ├─ models.json │
└──────────────────────┘ │ └─ settings.json │
└──────────────────────────┘
```
**Key design points**:
-- Pi Switch keeps its own SQLite database for the **full config** (all providers + all models, including inactive ones)
+- Kimi Switch keeps its own SQLite database for the **full config** (all providers + all models, including inactive ones)
- Clicking "Switch to" writes **only the active provider** into the agent's native config; the others are not written
- This way Kimi Code / Pi always see exactly one active provider and are never confused by managed/OAuth defaults
- The `raw_other` field passes unknown keys through untouched, so round-trips never lose fields
@@ -118,11 +118,11 @@ Credential precedence: the `api_key` field wins over the same-named key in the `
| File | Purpose | Backup |
| --- | --- | --- |
-| `%USERPROFILE%\.pi-switch\pi-switch.db` | Pi Switch's own SQLite database holding the full config | — |
+| `%USERPROFILE%\.kimi-switch\kimi-switch.db` | Kimi Switch's own SQLite database holding the full config | — |
| `%USERPROFILE%\.kimi-code\config.toml` | Kimi Code CLI's TOML config (**written on switch**) | `backups/config.toml.bak.{YYYYMMDD_HHMMSS}` next to it, kept 7 days |
| `%USERPROFILE%\.pi\agent\models.json` | Pi's provider+model config (**written on switch**) | `backups/models.json.bak.{YYYYMMDD_HHMMSS}` next to it, kept 7 days |
| `%USERPROFILE%\.pi\agent\settings.json` | Pi's default provider/model (**written on switch**) | `backups/settings.json.bak.{YYYYMMDD_HHMMSS}` next to it, kept 7 days |
-| `localStorage[pi-switch-agent]` | Frontend remembers the last selected agent (kimi_code / pi) | — |
+| `localStorage[kimi-switch-agent]` | Frontend remembers the last selected agent (kimi_code / pi) | — |
Environment-variable overrides:
@@ -205,7 +205,7 @@ Good for pure UI debugging.
│ └── tauri.conf.json # Tauri config (window/bundle/CSP)
│
├── scripts/generate-icons.py # Generate all icon sizes from SVG
-├── public/pi.svg # App icon source (blue-purple gradient π)
+├── public/kimi.svg # App icon source (blue-purple gradient π)
└── docs/superpowers/ # Design specs & implementation plans
```
@@ -283,7 +283,7 @@ npm run tauri-build
Output:
-- `src-tauri/target/release/bundle/msi/Pi Switch_0.1.0_x64_en-US.msi`
+- `src-tauri/target/release/bundle/msi/Kimi Switch_0.1.0_x64_en-US.msi`
A Windows installer (MSI) with the WebView2 bootstrapper embedded for auto-download. `nsis` is disabled; only MSI is produced.
@@ -303,7 +303,7 @@ A: Run `/reload` inside the Kimi Code session (the CLI only re-reads `~/.kimi-co
A: A native `beforeunload` prompt appears before closing, and the title bar shows a `*` prefix.
**Q: How do I back up / migrate my config?**
-A: Back up `%USERPROFILE%\.pi-switch\pi-switch.db`; it holds the full config (including inactive providers).
+A: Back up `%USERPROFILE%\.kimi-switch\kimi-switch.db`; it holds the full config (including inactive providers).
**Q: Why can't Vertex AI fetch the model list?**
A: Vertex requires GCP project/location credentials. The current implementation leaves a TODO pending GCP SDK integration.
@@ -314,7 +314,7 @@ A: The code doesn't depend on Windows-only APIs, but `tauri.conf.json` only targ
## Security Notes
- API keys are stored in plaintext in the local SQLite database and agent native configs — **do not store them on shared computers**
-- Do not commit `pi-switch.db`, `config.toml`, or `models.json` to Git
+- Do not commit `kimi-switch.db`, `config.toml`, or `models.json` to Git
- The app CSP is tightened (`default-src 'self'`), but WebView2 may still cache form content — log out when finished on public machines
---
diff --git a/docs/superpowers/plans/2026-07-07-i18n-ui-agent-switch.md b/docs/superpowers/plans/2026-07-07-i18n-ui-agent-switch.md
index 6567850..489bbb9 100644
--- a/docs/superpowers/plans/2026-07-07-i18n-ui-agent-switch.md
+++ b/docs/superpowers/plans/2026-07-07-i18n-ui-agent-switch.md
@@ -229,16 +229,16 @@ Search `src/components/` and `src/App.tsx` for any literal Chinese or English UI
**Files:**
- Modify: `index.html`
-- Create/overwrite: `public/pi.svg`
+- 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 Pi Switch icons**
+- [ ] **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 `/pi.svg`.
+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": "Pi Switch"`. Ensure `index.html` `
` is also `Pi Switch`.
+`tauri.conf.json` already has `"title": "Kimi Switch"`. Ensure `index.html` `` is also `Kimi Switch`.
---
diff --git a/docs/superpowers/specs/2026-07-09-kimicode-global-settings-panel-design.md b/docs/superpowers/specs/2026-07-09-kimicode-global-settings-panel-design.md
index e9ecbf7..eed26fc 100644
--- a/docs/superpowers/specs/2026-07-09-kimicode-global-settings-panel-design.md
+++ b/docs/superpowers/specs/2026-07-09-kimicode-global-settings-panel-design.md
@@ -2,7 +2,7 @@
## 背景
-Kimi Code CLI 的 `~/.kimi-code/config.toml` 包含大量全局设置:`[thinking]`、`[loop_control]`、`[background]`、`[[permission.rules]]`、`[[hooks]]` 等。当前 Pi Switch 只暴露了 `providers` 和 `models` 的可视化编辑,其他全局配置只能通过"配置 JSON"标签页手动修改,不够便捷。
+Kimi Code CLI 的 `~/.kimi-code/config.toml` 包含大量全局设置:`[thinking]`、`[loop_control]`、`[background]`、`[[permission.rules]]`、`[[hooks]]` 等。当前 Kimi Switch 只暴露了 `providers` 和 `models` 的可视化编辑,其他全局配置只能通过"配置 JSON"标签页手动修改,不够便捷。
## 目标
diff --git a/index.html b/index.html
index 868db57..f04854c 100644
--- a/index.html
+++ b/index.html
@@ -2,9 +2,9 @@
-
+
- Pi Switch
+ Kimi Switch
diff --git a/package-lock.json b/package-lock.json
index e15ae53..7e58c71 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "piswitch",
+ "name": "kimiswitch",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "piswitch",
+ "name": "kimiswitch",
"version": "0.1.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0",
diff --git a/package.json b/package.json
index 8f5fc39..f4f20bf 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "piswitch",
+ "name": "kimiswitch",
"private": true,
- "version": "0.1.0",
+ "version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/public/kimi.svg b/public/kimi.svg
new file mode 100644
index 0000000..19f6884
--- /dev/null
+++ b/public/kimi.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/public/pi.svg b/public/pi.svg
deleted file mode 100644
index 55a4412..0000000
--- a/public/pi.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
\ No newline at end of file
diff --git a/scripts/generate-icons.py b/scripts/generate-icons.py
index 6d7b0e0..80e530e 100644
--- a/scripts/generate-icons.py
+++ b/scripts/generate-icons.py
@@ -3,78 +3,106 @@ import os
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT_DIR = os.path.join(ROOT, "src-tauri", "icons")
-SVG_PATH = os.path.join(ROOT, "public", "pi.svg")
+SVG_PATH = os.path.join(ROOT, "public", "kimi.svg")
os.makedirs(OUT_DIR, exist_ok=True)
os.makedirs(os.path.dirname(SVG_PATH), exist_ok=True)
+# Brand colors: blue -> purple diagonal gradient
+C_TOP = (59, 130, 246) # #3b82f6
+C_BOT = (139, 92, 246) # #8b5cf6
+DOT = (255, 255, 255, 230)
+
+
+def lerp(a, b, t):
+ return int(a + (b - a) * t)
+
+
+def rounded_mask(size, radius):
+ m = Image.new("L", (size, size), 0)
+ d = ImageDraw.Draw(m)
+ d.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=255)
+ return m
+
+
def draw_icon(size):
- img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
+ # Render at 4x then downscale for smooth edges
+ S = size * 4
+ img = Image.new("RGBA", (S, S), (0, 0, 0, 0))
+
+ # Diagonal gradient background
+ grad = Image.new("RGBA", (S, S))
+ gd = ImageDraw.Draw(grad)
+ for y in range(S):
+ for x in range(0, S, 4): # step to speed up; visually identical after downscale
+ t = (x + y) / (2 * S)
+ gd.line([(x, y), (min(x + 3, S - 1), y)],
+ fill=(lerp(C_TOP[0], C_BOT[0], t),
+ lerp(C_TOP[1], C_BOT[1], t),
+ lerp(C_TOP[2], C_BOT[2], t), 255))
+ img.paste(grad, (0, 0), rounded_mask(S, int(S * 0.22)))
+
draw = ImageDraw.Draw(img)
- # Gradient-ish circular background: blue -> purple
- for y in range(size):
- ratio = y / size
- r = int(59 + (139 - 59) * ratio)
- g = int(130 + (92 - 130) * ratio)
- b = int(246 + (246 - 246) * ratio)
- draw.line([(0, y), (size, y)], fill=(r, g, b, 255))
-
- # Clip to circle
- mask = Image.new("L", (size, size), 0)
- mask_draw = ImageDraw.Draw(mask)
- mask_draw.ellipse((0, 0, size, size), fill=255)
- img.putalpha(mask)
-
- # Draw π symbol
- font_size = int(size * 0.55)
- try:
- font = ImageFont.truetype("segoeui.ttf", font_size)
- except Exception:
+ # Bold "K"
+ font_size = int(S * 0.52)
+ font = None
+ for name in ("segoeuib.ttf", "arialbd.ttf", "DejaVuSans-Bold.ttf"):
try:
- font = ImageFont.truetype("arial.ttf", font_size)
+ font = ImageFont.truetype(name, font_size)
+ break
except Exception:
- font = ImageFont.load_default()
+ continue
+ if font is None:
+ font = ImageFont.load_default()
- draw = ImageDraw.Draw(img)
- text = "π"
- bbox = draw.textbbox((0, 0), text, font=font)
- text_w = bbox[2] - bbox[0]
- text_h = bbox[3] - bbox[1]
- x = (size - text_w) // 2 - bbox[0]
- y = (size - text_h) // 2 - bbox[1]
- draw.text((x, y), text, font=font, fill=(255, 255, 255, 255))
- return img
+ bbox = draw.textbbox((0, 0), "K", font=font)
+ tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
+ # Optical center: shift slightly left/down to balance the toggle dot
+ x = (S - tw) // 2 - bbox[0] - int(S * 0.02)
+ y = (S - th) // 2 - bbox[1] + int(S * 0.02)
+ draw.text((x, y), "K", font=font, fill=(255, 255, 255, 255))
-sizes = [32, 128, 256]
-for s in sizes:
+ # Toggle dot (top-right) — the "switch" cue
+ cx, cy = int(S * 0.74), int(S * 0.26)
+ r = int(S * 0.075)
+ ring = int(S * 0.018)
+ draw.ellipse((cx - r - ring, cy - r - ring, cx + r + ring, cy + r + ring),
+ fill=(255, 255, 255, 90))
+ draw.ellipse((cx - r, cy - r, cx + r, cy + r), fill=DOT)
+
+ return img.resize((size, size), Image.LANCZOS)
+
+
+# --- Tauri bundle icons ---
+for s in (32, 128, 256):
img = draw_icon(s)
- if s == 128:
- img.save(os.path.join(OUT_DIR, "128x128.png"))
- img2x = draw_icon(256)
- img2x.save(os.path.join(OUT_DIR, "128x128@2x.png"))
- elif s == 32:
+ if s == 32:
img.save(os.path.join(OUT_DIR, "32x32.png"))
+ elif s == 128:
+ img.save(os.path.join(OUT_DIR, "128x128.png"))
+ draw_icon(256).save(os.path.join(OUT_DIR, "128x128@2x.png"))
+ else:
+ img.save(os.path.join(OUT_DIR, "icon.png"))
# Multi-size ICO
ico_sizes = [(32, 32), (64, 64), (128, 128), (256, 256)]
imgs = [draw_icon(s[0]) for s in ico_sizes]
-imgs[0].save(
- os.path.join(OUT_DIR, "icon.ico"),
- format="ICO",
- sizes=ico_sizes,
-)
+imgs[0].save(os.path.join(OUT_DIR, "icon.ico"), format="ICO", sizes=ico_sizes)
-# Simple SVG favicon for the web
+# --- Web favicon / app SVG ---
svg = ''''''
with open(SVG_PATH, "w", encoding="utf-8") as f:
f.write(svg)
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index d768329..0985076 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "kimiswitch"
-version = "0.1.0"
-description = "Pi Switch - model config manager"
+version = "0.2.0"
+description = "Kimi Switch - model config manager"
authors = ["you"]
edition = "2021"
@@ -9,7 +9,7 @@ edition = "2021"
tauri-build = { version = "2.0.0", features = [] }
[dependencies]
-tauri = { version = "2.0.0", features = [] }
+tauri = { version = "2.0.0", features = ["tray-icon"] }
tauri-plugin-opener = "2.0.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png
index 5a75484..7519dc8 100644
Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ
diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png
index 3d539e7..b6d95f1 100644
Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ
diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png
index 274362a..7b05c84 100644
Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ
diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico
index c767b39..d0b55ea 100644
Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ
diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png
index 8d0752a..b6d95f1 100644
Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index 5751265..bb7d3fa 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -18,7 +18,7 @@ pub fn debug_log(message: String) {
#[tauri::command]
pub fn load_agent_config_command(agent: Agent) -> Result {
- // Load from Pi Switch's own SQLite database first.
+ // Load from Kimi Switch's own SQLite database first.
match db::load_config(&agent) {
Ok(config) if !config.providers.is_empty() => Ok(config),
_ => {
@@ -51,7 +51,7 @@ pub fn load_agent_config_command(agent: Agent) -> Result {
#[tauri::command]
pub fn save_agent_config_command(agent: Agent, config: Config) -> Result<(), String> {
- // Save the full Pi Switch configuration to local SQLite.
+ // Save the full Kimi Switch configuration to local SQLite.
db::save_config(&agent, &config).map_err(fmt_anyhow)
}
@@ -88,7 +88,7 @@ fn active_provider_and_model(config: &Config) -> Option<(String, String)> {
fn build_active_config(config: &Config) -> Config {
// Only the provider explicitly marked as active is written to the agent's
- // native config. This ensures Kimi Code / Pi follow Pi Switch's choice
+ // native config. This ensures Kimi Code / Pi follow Kimi Switch's choice
// instead of falling back to a managed/native provider.
let providers: IndexMap = config
.providers
diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs
index 9db11f4..03223cd 100644
--- a/src-tauri/src/db.rs
+++ b/src-tauri/src/db.rs
@@ -1,6 +1,6 @@
-//! Pi Switch local SQLite storage.
+//! Kimi Switch local SQLite storage.
//!
-//! This module stores the full Pi Switch configuration (all providers and models
+//! This module stores the full Kimi Switch configuration (all providers and models
//! for both Kimi Code and Pi agents) in a local SQLite database. It is separate
//! from the agent-specific config files that are written when the user activates
//! a provider.
@@ -16,17 +16,43 @@ use crate::models::{Agent, Config, Model, Provider, ProviderType};
pub type DbResult = anyhow::Result;
-pub fn pi_switch_data_dir() -> PathBuf {
+pub fn kimi_switch_data_dir() -> PathBuf {
dirs::home_dir()
- .map(|h| h.join(".pi-switch"))
+ .map(|h| h.join(".kimi-switch"))
.expect("failed to resolve home directory")
}
pub fn db_path() -> PathBuf {
- pi_switch_data_dir().join("pi-switch.db")
+ kimi_switch_data_dir().join("kimi-switch.db")
+}
+
+/// One-time migration: if the legacy `~/.pi-switch/` data directory exists and
+/// `~/.kimi-switch/` does not, move it so existing users keep their saved
+/// configuration. Safe to call on every startup — it is a no-op once the new
+/// directory exists.
+fn migrate_legacy_data_dir() {
+ let new_dir = kimi_switch_data_dir();
+ if new_dir.exists() {
+ return;
+ }
+ let old_dir = match dirs::home_dir() {
+ Some(h) => h.join(".pi-switch"),
+ None => return,
+ };
+ if !old_dir.exists() {
+ return;
+ }
+ // Best-effort move; failures are silently ignored so the app can still start.
+ if let Some(new_parent) = new_dir.parent() {
+ if let Err(_) = std::fs::create_dir_all(new_parent) {
+ return;
+ }
+ }
+ let _ = std::fs::rename(&old_dir, &new_dir);
}
pub fn init_db() -> DbResult {
+ migrate_legacy_data_dir();
let path = db_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
@@ -64,7 +90,6 @@ pub fn init_db() -> DbResult {
model TEXT NOT NULL,
max_context_size INTEGER NOT NULL,
display_name TEXT,
- role TEXT,
supports_1m INTEGER NOT NULL DEFAULT 0,
capabilities TEXT,
raw_other TEXT,
@@ -128,20 +153,19 @@ pub fn load_config(agent: &Agent) -> DbResult {
let mut models = IndexMap::new();
{
let mut stmt = tx.prepare(
- "SELECT alias, provider_name, model, max_context_size, display_name, role, supports_1m, capabilities, raw_other
+ "SELECT alias, provider_name, model, max_context_size, display_name, supports_1m, capabilities, raw_other
FROM models WHERE agent = ?1 ORDER BY id",
)?;
let model_rows = stmt.query_map(params![agent.as_str()], |row| {
- let caps_json: Option = row.get(7)?;
- let raw_json: Option = row.get(8)?;
+ let caps_json: Option = row.get(6)?;
+ let raw_json: Option = row.get(7)?;
Ok(Model {
alias: row.get(0)?,
provider: row.get(1)?,
model: row.get(2)?,
max_context_size: row.get::<_, i64>(3)? as u64,
display_name: row.get(4)?,
- role: row.get(5)?,
- supports_1m: row.get::<_, i32>(6)? != 0,
+ supports_1m: row.get::<_, i32>(5)? != 0,
capabilities: caps_json
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default(),
@@ -202,8 +226,8 @@ pub fn save_config(agent: &Agent, config: &Config) -> DbResult<()> {
{
let mut insert_model = tx.prepare(
"INSERT INTO models
- (agent, alias, provider_name, model, max_context_size, display_name, role, supports_1m, capabilities, raw_other)
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
+ (agent, alias, provider_name, model, max_context_size, display_name, supports_1m, capabilities, raw_other)
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
)?;
for model in config.models.values() {
@@ -214,7 +238,6 @@ pub fn save_config(agent: &Agent, config: &Config) -> DbResult<()> {
model.model,
model.max_context_size as i64,
model.display_name,
- model.role,
model.supports_1m as i32,
serde_json::to_string(&model.capabilities).ok(),
serde_json::to_string(&model.raw_other).ok(),
diff --git a/src-tauri/src/kimi_code_io.rs b/src-tauri/src/kimi_code_io.rs
index 82cbcf9..c4d5d8d 100644
--- a/src-tauri/src/kimi_code_io.rs
+++ b/src-tauri/src/kimi_code_io.rs
@@ -2,7 +2,7 @@
//!
//! Kimi Code stores its configuration in `~/.kimi-code/config.toml`.
//! This module reads/writes that file and converts between Kimi's TOML
-//! format and Pi Switch's internal `Config`/`Provider`/`Model` types.
+//! format and Kimi Switch's internal `Config`/`Provider`/`Model` types.
use std::path::PathBuf;
@@ -64,7 +64,7 @@ pub fn save_kimi_code_config(value: &TomlValue) -> KimiResult<()> {
Ok(())
}
-/// Import a Kimi Code TOML config into Pi Switch's internal `Config`.
+/// Import a Kimi Code TOML config into Kimi Switch's internal `Config`.
pub fn kimi_code_to_config(value: &TomlValue) -> Config {
let root = value.as_table().cloned().unwrap_or_default();
@@ -76,6 +76,72 @@ pub fn kimi_code_to_config(value: &TomlValue) -> Config {
let mut providers = IndexMap::new();
let mut models = IndexMap::new();
+ // Read models first so we can determine which provider the default_model
+ // belongs to. Only that provider should be marked active in the UI.
+ if let Some(models_table) = root.get("models").and_then(|v| v.as_table()) {
+ for (alias, mv) in models_table {
+ let table = mv.as_table().cloned().unwrap_or_default();
+ let provider = table
+ .get("provider")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string())
+ .unwrap_or_default();
+ let model_id = table
+ .get("model")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string())
+ .unwrap_or_default();
+ let max_context_size = table
+ .get("max_context_size")
+ .and_then(|v| v.as_integer())
+ .map(|n| n as u64)
+ .unwrap_or(128_000);
+ let display_name = table
+ .get("display_name")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string());
+ let capabilities: Vec = table
+ .get("capabilities")
+ .and_then(|v| v.as_array())
+ .map(|arr| {
+ arr.iter()
+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
+ .collect()
+ })
+ .unwrap_or_default();
+ let supports_1m = max_context_size >= 1_000_000;
+
+ let raw_other = {
+ let mut rest = table.clone();
+ rest.remove("provider");
+ rest.remove("model");
+ rest.remove("max_context_size");
+ rest.remove("display_name");
+ rest.remove("capabilities");
+ toml_value_to_json(&TomlValue::Table(rest))
+ };
+
+ models.insert(
+ alias.clone(),
+ Model {
+ alias: alias.clone(),
+ provider,
+ model: model_id,
+ max_context_size,
+ display_name,
+ supports_1m,
+ capabilities,
+ raw_other,
+ },
+ );
+ }
+ }
+
+ let active_provider_name = default_model
+ .as_ref()
+ .and_then(|alias| models.get(alias))
+ .map(|m| m.provider.as_str());
+
if let Some(providers_table) = root.get("providers").and_then(|v| v.as_table()) {
for (name, pv) in providers_table {
let table = pv.as_table().cloned().unwrap_or_default();
@@ -125,68 +191,7 @@ pub fn kimi_code_to_config(value: &TomlValue) -> Config {
official_url: None,
managed,
enabled,
- active: true,
- raw_other,
- },
- );
- }
- }
-
- if let Some(models_table) = root.get("models").and_then(|v| v.as_table()) {
- for (alias, mv) in models_table {
- let table = mv.as_table().cloned().unwrap_or_default();
- let provider = table
- .get("provider")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string())
- .unwrap_or_default();
- let model_id = table
- .get("model")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string())
- .unwrap_or_default();
- let max_context_size = table
- .get("max_context_size")
- .and_then(|v| v.as_integer())
- .map(|n| n as u64)
- .unwrap_or(128_000);
- let display_name = table
- .get("display_name")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
- let capabilities: Vec = table
- .get("capabilities")
- .and_then(|v| v.as_array())
- .map(|arr| {
- arr.iter()
- .filter_map(|v| v.as_str().map(|s| s.to_string()))
- .collect()
- })
- .unwrap_or_default();
- let supports_1m = capabilities.iter().any(|c| c == "thinking")
- || max_context_size >= 1_000_000;
-
- let raw_other = {
- let mut rest = table.clone();
- rest.remove("provider");
- rest.remove("model");
- rest.remove("max_context_size");
- rest.remove("display_name");
- rest.remove("capabilities");
- toml_value_to_json(&TomlValue::Table(rest))
- };
-
- models.insert(
- alias.clone(),
- Model {
- alias: alias.clone(),
- provider,
- model: model_id,
- max_context_size,
- display_name,
- role: None,
- supports_1m,
- capabilities,
+ active: active_provider_name == Some(name.as_str()),
raw_other,
},
);
@@ -209,7 +214,7 @@ pub fn kimi_code_to_config(value: &TomlValue) -> Config {
}
}
-/// Export Pi Switch's internal `Config` to a Kimi Code TOML config value.
+/// Export Kimi Switch's internal `Config` to a Kimi Code TOML config value.
///
/// If `existing` is provided, unknown top-level sections (e.g. `services`) are
/// preserved; otherwise a fresh TOML table is used.
@@ -230,7 +235,7 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
let mut providers_table = Table::new();
for (name, provider) in &config.providers {
// Only write the active provider to Kimi Code config so the agent
- // follows Pi Switch's selection.
+ // follows Kimi Switch's selection.
if !provider.active {
continue;
}
@@ -299,10 +304,7 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
if let Some(display_name) = model.display_name.clone().filter(|s| !s.is_empty()) {
mt.insert("display_name".to_string(), TomlValue::String(display_name));
}
- let mut capabilities = model.capabilities.clone();
- if model.supports_1m && !capabilities.iter().any(|c| c == "thinking") {
- capabilities.push("thinking".to_string());
- }
+ let capabilities = model.capabilities.clone();
if !capabilities.is_empty() {
mt.insert(
"capabilities".to_string(),
@@ -461,7 +463,8 @@ api_key = ""
assert_eq!(model.model, "kimi-for-coding");
assert_eq!(model.max_context_size, 262144);
assert!(model.capabilities.contains(&"thinking".to_string()));
- assert!(model.supports_1m);
+ // supports_1m reflects context size only, not thinking capability.
+ assert!(!model.supports_1m);
// Unknown top-level sections are preserved.
let services = config.raw_other.get("services");
@@ -496,7 +499,6 @@ api_key = ""
model: "glm-5.2".to_string(),
max_context_size: 900_000,
display_name: None,
- role: None,
supports_1m: true,
capabilities: vec!["thinking".to_string()],
raw_other: Value::Null,
@@ -527,4 +529,62 @@ api_key = ""
let caps = model.get("capabilities").unwrap().as_array().unwrap();
assert!(caps.iter().any(|v| v.as_str() == Some("thinking")));
}
+
+ #[test]
+ fn kimi_code_export_does_not_inject_thinking_from_supports_1m() {
+ let mut providers = IndexMap::new();
+ providers.insert(
+ "test-provider".to_string(),
+ Provider {
+ name: "test-provider".to_string(),
+ provider_type: ProviderType::Anthropic,
+ base_url: Some("https://example.com".to_string()),
+ api_key: Some("sk-test".to_string()),
+ env: IndexMap::new(),
+ note: None,
+ official_url: None,
+ managed: false,
+ enabled: true,
+ active: true,
+ raw_other: Value::Null,
+ },
+ );
+ let mut models = IndexMap::new();
+ models.insert(
+ "big-model".to_string(),
+ Model {
+ alias: "big-model".to_string(),
+ provider: "test-provider".to_string(),
+ model: "big-model".to_string(),
+ max_context_size: 2_000_000,
+ display_name: None,
+ supports_1m: true,
+ capabilities: vec![],
+ raw_other: Value::Null,
+ },
+ );
+ let config = Config {
+ default_model: Some("big-model".to_string()),
+ providers,
+ models,
+ raw_other: Value::Null,
+ };
+
+ let exported = config_to_kimi_code(&config, None);
+ let root = exported.as_table().unwrap();
+ let models_table = root.get("models").unwrap().as_table().unwrap();
+ let model = models_table.get("big-model").unwrap().as_table().unwrap();
+ // supports_1m=true but capabilities empty → must NOT inject "thinking".
+ let caps = model.get("capabilities");
+ match caps {
+ None => {}
+ Some(v) => {
+ let arr = v.as_array().unwrap();
+ assert!(
+ !arr.iter().any(|c| c.as_str() == Some("thinking")),
+ "thinking should not be injected from supports_1m"
+ );
+ }
+ }
+ }
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 25d938e..5033eb1 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -5,9 +5,82 @@ pub mod kimi_code_io;
pub mod models;
pub mod pi_io;
+use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
+use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder};
+use tauri::Manager;
+
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
+ .setup(|app| {
+ println!("[Tauri] Setup started");
+ let window = app.get_webview_window("main").unwrap();
+ println!("[Tauri] Window label: {}", window.label());
+
+ // Tray menu: show / separator / quit
+ let show_i = MenuItem::with_id(app, "show", "显示", true, None::<&str>)?;
+ let quit_i = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
+ let menu = Menu::with_items(
+ app,
+ &[&show_i, &PredefinedMenuItem::separator(app)?, &quit_i],
+ )?;
+
+ // Tray icon: reuse the window icon
+ let icon = app.default_window_icon().unwrap().clone();
+ TrayIconBuilder::new()
+ .icon(icon)
+ .menu(&menu)
+ .show_menu_on_left_click(false)
+ .on_menu_event(|app, event| match event.id.as_ref() {
+ "show" => {
+ if let Some(window) = app.get_webview_window("main") {
+ let _ = window.show();
+ let _ = window.unminimize();
+ let _ = window.set_focus();
+ }
+ }
+ "quit" => {
+ app.exit(0);
+ }
+ _ => {}
+ })
+ .on_tray_icon_event(|tray, event| {
+ if let tauri::tray::TrayIconEvent::Click {
+ button,
+ button_state,
+ ..
+ } = event
+ {
+ if button == MouseButton::Left && button_state == MouseButtonState::Up {
+ let app = tray.app_handle();
+ if let Some(window) = app.get_webview_window("main") {
+ if window.is_visible().unwrap_or(true) {
+ let _ = window.hide();
+ } else {
+ let _ = window.show();
+ let _ = window.unminimize();
+ let _ = window.set_focus();
+ }
+ }
+ }
+ }
+ })
+ .build(app)?;
+
+ // Minimize to tray: restore then hide so the taskbar button disappears
+ let window_clone = window.clone();
+ window.on_window_event(move |event| match event {
+ tauri::WindowEvent::Resized(_) => {
+ if window_clone.is_minimized().unwrap_or(false) {
+ let _ = window_clone.unminimize();
+ let _ = window_clone.hide();
+ }
+ }
+ _ => {}
+ });
+
+ Ok(())
+ })
.invoke_handler(tauri::generate_handler![
commands::load_agent_config_command,
commands::save_agent_config_command,
diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs
index ed9cc98..6ff5eba 100644
--- a/src-tauri/src/models.rs
+++ b/src-tauri/src/models.rs
@@ -129,8 +129,6 @@ pub struct Model {
pub model: String,
pub max_context_size: u64,
pub display_name: Option,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub role: Option,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub supports_1m: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -147,7 +145,6 @@ impl std::fmt::Debug for Model {
.field("model", &self.model)
.field("max_context_size", &self.max_context_size)
.field("display_name", &self.display_name)
- .field("role", &self.role)
.field("supports_1m", &self.supports_1m)
.field("capabilities", &self.capabilities)
.field("raw_other", &"")
@@ -162,7 +159,6 @@ impl PartialEq for Model {
&& self.model == other.model
&& self.max_context_size == other.max_context_size
&& self.display_name == other.display_name
- && self.role == other.role
&& self.supports_1m == other.supports_1m
&& self.capabilities == other.capabilities
&& self.raw_other == other.raw_other
diff --git a/src-tauri/src/pi_io.rs b/src-tauri/src/pi_io.rs
index 6fcd0fb..7f8cb1a 100644
--- a/src-tauri/src/pi_io.rs
+++ b/src-tauri/src/pi_io.rs
@@ -2,7 +2,7 @@
//!
//! Pi stores custom providers and models in `~/.pi/agent/models.json`.
//! This module reads and writes that file and converts between Pi's JSON
-//! format and Pi Switch's internal `Config`/`Provider`/`Model` types.
+//! format and Kimi Switch's internal `Config`/`Provider`/`Model` types.
use std::path::PathBuf;
@@ -150,7 +150,7 @@ pub fn pi_settings_path() -> PathBuf {
pi_agent_dir().join("settings.json")
}
-/// Pi's `settings.json` file (only the fields Pi Switch manipulates).
+/// Pi's `settings.json` file (only the fields Kimi Switch manipulates).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PiSettingsFile {
#[serde(rename = "defaultProvider", default, skip_serializing_if = "Option::is_none")]
@@ -197,7 +197,7 @@ pub fn save_pi_settings(file: &PiSettingsFile) -> PiResult<()> {
Ok(())
}
-/// Convert a Pi Switch provider type to the Pi `api` field value.
+/// Convert a Kimi Switch provider type to the Pi `api` field value.
pub fn pi_api_for_provider(provider_type: &ProviderType) -> &'static str {
match provider_type {
ProviderType::Openai => "openai-completions",
@@ -210,7 +210,7 @@ pub fn pi_api_for_provider(provider_type: &ProviderType) -> &'static str {
}
}
-/// Convert a Pi `api` field value back to a Pi Switch provider type.
+/// Convert a Pi `api` field value back to a Kimi Switch provider type.
pub fn provider_type_for_pi_api(api: &str) -> ProviderType {
match api {
"openai-responses" => ProviderType::OpenaiResponses,
@@ -222,7 +222,7 @@ pub fn provider_type_for_pi_api(api: &str) -> ProviderType {
}
/// Merge provider-level fields that have dedicated `PiProvider` fields into the
-/// `raw_other` blob so they survive the round-trip through Pi Switch's internal
+/// `raw_other` blob so they survive the round-trip through Kimi Switch's internal
/// `Config`.
fn merge_provider_known_into_raw(pi_provider: &PiProvider, raw: &mut Value) {
let has_known = pi_provider.headers.is_some()
@@ -249,7 +249,7 @@ fn merge_provider_known_into_raw(pi_provider: &PiProvider, raw: &mut Value) {
}
/// Merge model-level fields that have dedicated `PiModel` fields into the
-/// `raw_other` blob so they survive the round-trip through Pi Switch's internal
+/// `raw_other` blob so they survive the round-trip through Kimi Switch's internal
/// `Config`.
fn merge_model_known_into_raw(pi_model: &PiModel, raw: &mut Value) {
let has_known = pi_model.cost.is_some()
@@ -310,7 +310,7 @@ fn extract_model_fields(raw: &Value) -> (Option, Option, Option Config {
let mut providers = IndexMap::new();
let mut models = IndexMap::new();
@@ -364,7 +364,6 @@ pub fn pi_file_to_config(file: &PiModelsFile) -> Config {
model: pi_model.id.clone(),
max_context_size: pi_model.context_window,
display_name,
- role: None,
supports_1m: pi_model.reasoning || pi_model.context_window >= 1_000_000,
capabilities: vec![],
raw_other: model_raw,
@@ -383,7 +382,7 @@ pub fn pi_file_to_config(file: &PiModelsFile) -> Config {
}
}
-/// Export a Pi Switch `Config` to a Pi `models.json`.
+/// Export a Kimi Switch `Config` to a Pi `models.json`.
pub fn config_to_pi_file(config: &Config) -> PiModelsFile {
let mut providers = IndexMap::new();
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 8d60681..e009e93 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
- "productName": "Pi Switch",
- "version": "0.1.0",
- "identifier": "com.piswitch.app",
+ "productName": "Kimi Switch",
+ "version": "0.2.0",
+ "identifier": "com.kimiswitch.app",
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
@@ -11,7 +11,7 @@
"app": {
"windows": [
{
- "title": "Pi Switch",
+ "title": "Kimi Switch",
"width": 1200,
"height": 800,
"minWidth": 1000,
diff --git a/src/App.tsx b/src/App.tsx
index 8d8b12a..43863db 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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 | undefined;
@@ -65,6 +65,7 @@ export default function App() {
const [view, setView] = useState<"list" | "edit">("list");
const [editingProvider, setEditingProvider] = useState("");
const [loadTimeout, setLoadTimeout] = useState(false);
+ const [switchMessage, setSwitchMessage] = useState(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 = {};
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() {
)}
+ {switchMessage && (
+
+ {switchMessage}
+
+ )}
+
{view === "list" ? (
{
- 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"] : [],
},
},
}));
diff --git a/src/components/ProviderEdit.tsx b/src/components/ProviderEdit.tsx
index 94b2d1f..c9156c5 100644
--- a/src/components/ProviderEdit.tsx
+++ b/src/components/ProviderEdit.tsx
@@ -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(null);
const [selected, setSelected] = useState>(new Set());
const [discoverError, setDiscoverError] = useState(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({
{t("discoveredModels", { count: discovered.length })}
+
{discovered.map((m) => (