feat: v0.2.0 - add system tray, clean up unused role field
- Replace text '+' icon with centered SVG plus icon for the add-provider button - Add system tray: minimize-to-tray and quit-from-tray menu (minimize hides window, close exits app) - Remove unused model role field (no functional effect, only stored/displayed) - Fix multiple providers showing as active in Kimi Code mode: only the default_model's provider is marked active - Copy /reload to clipboard after provider switch as a convenience hint - Bump version to 0.2.0
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
# Pi Switch
|
# Kimi Switch
|
||||||
|
|
||||||
> Windows 桌面端 LLM 供应商配置管理器,让你在 **Kimi Code CLI** 和 **Pi** 两个 Agent 之间无缝切换多家 LLM 供应商与模型。
|
> 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,容易出错。
|
1. **多供应商管理繁琐**:在 Kimi / Anthropic / OpenAI / Google GenAI / 自建代理 等多家供应商之间切换时,需要反复手改 TOML/JSON,容易出错。
|
||||||
2. **多 Agent 配置割裂**:同时使用 **Kimi Code CLI** 和 **Pi** 两个 Agent 的开发者,每个 Agent 都有一套独立的配置格式,改一处要改两份。
|
2. **多 Agent 配置割裂**:同时使用 **Kimi Code CLI** 和 **Pi** 两个 Agent 的开发者,每个 Agent 都有一套独立的配置格式,改一处要改两份。
|
||||||
|
|
||||||
Pi Switch 提供统一的图形界面:
|
Kimi Switch 提供统一的图形界面:
|
||||||
|
|
||||||
- 统一图形界面,分别管理 Kimi Code 和 Pi 两套 Agent 配置,顶部 Tab 切换
|
- 统一图形界面,分别管理 Kimi Code 和 Pi 两套 Agent 配置,顶部 Tab 切换
|
||||||
- 一键切换供应商,自动写入对应 Agent 的原生配置
|
- 一键切换供应商,自动写入对应 Agent 的原生配置
|
||||||
@@ -66,7 +66,7 @@ Pi Switch 提供统一的图形界面:
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌────────────────────────────────────────────────────────────────────┐
|
┌────────────────────────────────────────────────────────────────────┐
|
||||||
│ Pi Switch (Tauri v2) │
|
│ Kimi Switch (Tauri v2) │
|
||||||
│ │
|
│ │
|
||||||
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
|
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
|
||||||
│ │ React Frontend (TS) │ │ Rust Backend (lib.rs) │ │
|
│ │ React Frontend (TS) │ │ Rust Backend (lib.rs) │ │
|
||||||
@@ -86,17 +86,17 @@ Pi Switch 提供统一的图形界面:
|
|||||||
▼ ▼
|
▼ ▼
|
||||||
┌──────────────────────┐ ┌──────────────────────────┐
|
┌──────────────────────┐ ┌──────────────────────────┐
|
||||||
│ SQLite │ │ Agent 原生配置文件 │
|
│ SQLite │ │ Agent 原生配置文件 │
|
||||||
│ ~/.pi-switch/ │ │ ├─ ~/.kimi-code/ │
|
│ ~/.kimi-switch/ │ │ ├─ ~/.kimi-code/ │
|
||||||
│ pi-switch.db │ │ │ └─ config.toml │
|
│ kimi-switch.db │ │ │ └─ config.toml │
|
||||||
│ │ │ └─ ~/.pi/agent/ │
|
│ │ │ └─ ~/.pi/agent/ │
|
||||||
│ (Pi Switch 内部状态) │ │ ├─ models.json │
|
│ (Kimi Switch 内部状态) │ │ ├─ models.json │
|
||||||
└──────────────────────┘ │ └─ settings.json │
|
└──────────────────────┘ │ └─ settings.json │
|
||||||
└──────────────────────────┘
|
└──────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**关键设计**:
|
**关键设计**:
|
||||||
|
|
||||||
- Pi Switch 维护自己的 SQLite 数据库存**完整配置**(所有供应商 + 所有模型,含未激活的)
|
- Kimi Switch 维护自己的 SQLite 数据库存**完整配置**(所有供应商 + 所有模型,含未激活的)
|
||||||
- 点「切换使用」时只把**当前激活的供应商**写入 Agent 原生配置,其他不写
|
- 点「切换使用」时只把**当前激活的供应商**写入 Agent 原生配置,其他不写
|
||||||
- 这样 Kimi Code / Pi 始终只看到一个活跃供应商,不会被托管/OAuth 默认值干扰
|
- 这样 Kimi Code / Pi 始终只看到一个活跃供应商,不会被托管/OAuth 默认值干扰
|
||||||
- `raw_other` 字段透传未知键,保证前后往返不丢字段
|
- `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%\.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\models.json` | Pi 的供应商+模型配置(**切换时写入**) | 同目录下 `backups/models.json.bak.{YYYYMMDD_HHMMSS}`,保留 7 天 |
|
||||||
| `%USERPROFILE%\.pi\agent\settings.json` | Pi 的默认供应商/模型(**切换时写入**) | 同目录下 `backups/settings.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)
|
│ └── tauri.conf.json # Tauri 配置(窗口/打包/CSP)
|
||||||
│
|
│
|
||||||
├── scripts/generate-icons.py # 从 SVG 生成各尺寸图标
|
├── scripts/generate-icons.py # 从 SVG 生成各尺寸图标
|
||||||
├── public/pi.svg # 应用图标源(蓝紫渐变 π)
|
├── public/kimi.svg # 应用图标源(蓝紫渐变 π)
|
||||||
└── docs/superpowers/ # 设计规范与实施计划
|
└── 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。
|
Windows 安装包(MSI),含 WebView2 bootstrapper 自动下载。`nsis` 已禁用,目前只产 MSI。
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ A: 在 Kimi Code 会话里执行 `/reload`(Kimi Code CLI 才会重新读取 `~
|
|||||||
A: 关闭窗口前会弹原生 `beforeunload` 提示,标题栏也会显示 `*` 前缀。
|
A: 关闭窗口前会弹原生 `beforeunload` 提示,标题栏也会显示 `*` 前缀。
|
||||||
|
|
||||||
**Q: 怎么备份/迁移我的配置?**
|
**Q: 怎么备份/迁移我的配置?**
|
||||||
A: 备份 `%USERPROFILE%\.pi-switch\pi-switch.db` 即可,里面存了全量配置(含未激活的供应商)。
|
A: 备份 `%USERPROFILE%\.kimi-switch\kimi-switch.db` 即可,里面存了全量配置(含未激活的供应商)。
|
||||||
|
|
||||||
**Q: Vertex AI 为什么不能拉取模型列表?**
|
**Q: Vertex AI 为什么不能拉取模型列表?**
|
||||||
A: Vertex 需要 GCP project/location 凭证,当前实现留了 TODO,等 GCP SDK 集成后再补。
|
A: Vertex 需要 GCP project/location 凭证,当前实现留了 TODO,等 GCP SDK 集成后再补。
|
||||||
@@ -314,7 +314,7 @@ A: 代码不依赖 Windows 专属 API,但 `tauri.conf.json` 的 bundle 目标
|
|||||||
## 安全提示
|
## 安全提示
|
||||||
|
|
||||||
- API Key 以明文存储在本地 SQLite 和 Agent 原生配置里——**不要在共享电脑上保存**
|
- 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 仍可能缓存表单内容,注意在公共电脑用完退出
|
- 应用 CSP 已收紧(`default-src 'self'`),但 WebView2 仍可能缓存表单内容,注意在公共电脑用完退出
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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.
|
> 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
|
## 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.
|
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.
|
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 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
|
- 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) │ │
|
│ │ React Frontend (TS) │ │ Rust Backend (lib.rs) │ │
|
||||||
@@ -86,17 +86,17 @@ Pi Switch provides a unified GUI:
|
|||||||
▼ ▼
|
▼ ▼
|
||||||
┌──────────────────────┐ ┌──────────────────────────┐
|
┌──────────────────────┐ ┌──────────────────────────┐
|
||||||
│ SQLite │ │ Agent native configs │
|
│ SQLite │ │ Agent native configs │
|
||||||
│ ~/.pi-switch/ │ │ ├─ ~/.kimi-code/ │
|
│ ~/.kimi-switch/ │ │ ├─ ~/.kimi-code/ │
|
||||||
│ pi-switch.db │ │ │ └─ config.toml │
|
│ kimi-switch.db │ │ │ └─ config.toml │
|
||||||
│ │ │ └─ ~/.pi/agent/ │
|
│ │ │ └─ ~/.pi/agent/ │
|
||||||
│ (Pi Switch internal)│ │ ├─ models.json │
|
│ (Kimi Switch internal)│ │ ├─ models.json │
|
||||||
└──────────────────────┘ │ └─ settings.json │
|
└──────────────────────┘ │ └─ settings.json │
|
||||||
└──────────────────────────┘
|
└──────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key design points**:
|
**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
|
- 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
|
- 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
|
- 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 |
|
| 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%\.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\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 |
|
| `%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:
|
Environment-variable overrides:
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ Good for pure UI debugging.
|
|||||||
│ └── tauri.conf.json # Tauri config (window/bundle/CSP)
|
│ └── tauri.conf.json # Tauri config (window/bundle/CSP)
|
||||||
│
|
│
|
||||||
├── scripts/generate-icons.py # Generate all icon sizes from SVG
|
├── 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
|
└── docs/superpowers/ # Design specs & implementation plans
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ npm run tauri-build
|
|||||||
|
|
||||||
Output:
|
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.
|
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.
|
A: A native `beforeunload` prompt appears before closing, and the title bar shows a `*` prefix.
|
||||||
|
|
||||||
**Q: How do I back up / migrate my config?**
|
**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?**
|
**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.
|
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
|
## Security Notes
|
||||||
|
|
||||||
- API keys are stored in plaintext in the local SQLite database and agent native configs — **do not store them on shared computers**
|
- 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
|
- The app CSP is tightened (`default-src 'self'`), but WebView2 may still cache form content — log out when finished on public machines
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -229,16 +229,16 @@ Search `src/components/` and `src/App.tsx` for any literal Chinese or English UI
|
|||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `index.html`
|
- 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`
|
- 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**
|
- [ ] **Step 6.2: Verify app title**
|
||||||
|
|
||||||
`tauri.conf.json` already has `"title": "Pi Switch"`. Ensure `index.html` `<title>` is also `Pi Switch`.
|
`tauri.conf.json` already has `"title": "Kimi Switch"`. Ensure `index.html` `<title>` is also `Kimi Switch`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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"标签页手动修改,不够便捷。
|
||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/pi.svg" />
|
<link rel="icon" type="image/svg+xml" href="/kimi.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Pi Switch</title>
|
<title>Kimi Switch</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "piswitch",
|
"name": "kimiswitch",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "piswitch",
|
"name": "kimiswitch",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "piswitch",
|
"name": "kimiswitch",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#3b82f6"/>
|
||||||
|
<stop offset="100%" stop-color="#8b5cf6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="100" height="100" rx="22" fill="url(#g)"/>
|
||||||
|
<text x="48" y="70" text-anchor="middle" font-size="52" font-weight="700"
|
||||||
|
fill="white" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">K</text>
|
||||||
|
<circle cx="74" cy="26" r="9" fill="white" opacity="0.35"/>
|
||||||
|
<circle cx="74" cy="26" r="6.5" fill="white" opacity="0.92"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 615 B |
@@ -1,10 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#3b82f6"/>
|
|
||||||
<stop offset="100%" stop-color="#8b5cf6"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<circle cx="50" cy="50" r="48" fill="url(#g)"/>
|
|
||||||
<text x="50" y="66" text-anchor="middle" font-size="50" fill="white" font-family="system-ui, -apple-system, sans-serif">π</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 444 B |
@@ -3,78 +3,106 @@ import os
|
|||||||
|
|
||||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
OUT_DIR = os.path.join(ROOT, "src-tauri", "icons")
|
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(OUT_DIR, exist_ok=True)
|
||||||
os.makedirs(os.path.dirname(SVG_PATH), 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):
|
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)
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
# Gradient-ish circular background: blue -> purple
|
# Bold "K"
|
||||||
for y in range(size):
|
font_size = int(S * 0.52)
|
||||||
ratio = y / size
|
font = None
|
||||||
r = int(59 + (139 - 59) * ratio)
|
for name in ("segoeuib.ttf", "arialbd.ttf", "DejaVuSans-Bold.ttf"):
|
||||||
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:
|
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype("arial.ttf", font_size)
|
font = ImageFont.truetype(name, font_size)
|
||||||
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
font = ImageFont.load_default()
|
continue
|
||||||
|
if font is None:
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
draw = ImageDraw.Draw(img)
|
bbox = draw.textbbox((0, 0), "K", font=font)
|
||||||
text = "π"
|
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||||
bbox = draw.textbbox((0, 0), text, font=font)
|
# Optical center: shift slightly left/down to balance the toggle dot
|
||||||
text_w = bbox[2] - bbox[0]
|
x = (S - tw) // 2 - bbox[0] - int(S * 0.02)
|
||||||
text_h = bbox[3] - bbox[1]
|
y = (S - th) // 2 - bbox[1] + int(S * 0.02)
|
||||||
x = (size - text_w) // 2 - bbox[0]
|
draw.text((x, y), "K", font=font, fill=(255, 255, 255, 255))
|
||||||
y = (size - text_h) // 2 - bbox[1]
|
|
||||||
draw.text((x, y), text, font=font, fill=(255, 255, 255, 255))
|
|
||||||
return img
|
|
||||||
|
|
||||||
sizes = [32, 128, 256]
|
# Toggle dot (top-right) — the "switch" cue
|
||||||
for s in sizes:
|
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)
|
img = draw_icon(s)
|
||||||
if s == 128:
|
if s == 32:
|
||||||
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:
|
|
||||||
img.save(os.path.join(OUT_DIR, "32x32.png"))
|
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
|
# Multi-size ICO
|
||||||
ico_sizes = [(32, 32), (64, 64), (128, 128), (256, 256)]
|
ico_sizes = [(32, 32), (64, 64), (128, 128), (256, 256)]
|
||||||
imgs = [draw_icon(s[0]) for s in ico_sizes]
|
imgs = [draw_icon(s[0]) for s in ico_sizes]
|
||||||
imgs[0].save(
|
imgs[0].save(os.path.join(OUT_DIR, "icon.ico"), format="ICO", sizes=ico_sizes)
|
||||||
os.path.join(OUT_DIR, "icon.ico"),
|
|
||||||
format="ICO",
|
|
||||||
sizes=ico_sizes,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Simple SVG favicon for the web
|
# --- Web favicon / app SVG ---
|
||||||
svg = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
svg = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
<stop offset="0%" stop-color="#3b82f6"/>
|
<stop offset="0%" stop-color="#3b82f6"/>
|
||||||
<stop offset="100%" stop-color="#8b5cf6"/>
|
<stop offset="100%" stop-color="#8b5cf6"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<circle cx="50" cy="50" r="48" fill="url(#g)"/>
|
<rect width="100" height="100" rx="22" fill="url(#g)"/>
|
||||||
<text x="50" y="66" text-anchor="middle" font-size="50" fill="white" font-family="system-ui, -apple-system, sans-serif">π</text>
|
<text x="48" y="70" text-anchor="middle" font-size="52" font-weight="700"
|
||||||
|
fill="white" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">K</text>
|
||||||
|
<circle cx="74" cy="26" r="9" fill="white" opacity="0.35"/>
|
||||||
|
<circle cx="74" cy="26" r="6.5" fill="white" opacity="0.92"/>
|
||||||
</svg>'''
|
</svg>'''
|
||||||
with open(SVG_PATH, "w", encoding="utf-8") as f:
|
with open(SVG_PATH, "w", encoding="utf-8") as f:
|
||||||
f.write(svg)
|
f.write(svg)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Pi Switch - model config manager"
|
description = "Kimi Switch - model config manager"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ edition = "2021"
|
|||||||
tauri-build = { version = "2.0.0", features = [] }
|
tauri-build = { version = "2.0.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2.0.0", features = [] }
|
tauri = { version = "2.0.0", features = ["tray-icon"] }
|
||||||
tauri-plugin-opener = "2.0.0"
|
tauri-plugin-opener = "2.0.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 484 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 506 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 15 KiB |
@@ -18,7 +18,7 @@ pub fn debug_log(message: String) {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn load_agent_config_command(agent: Agent) -> Result<Config, String> {
|
pub fn load_agent_config_command(agent: Agent) -> Result<Config, String> {
|
||||||
// Load from Pi Switch's own SQLite database first.
|
// Load from Kimi Switch's own SQLite database first.
|
||||||
match db::load_config(&agent) {
|
match db::load_config(&agent) {
|
||||||
Ok(config) if !config.providers.is_empty() => Ok(config),
|
Ok(config) if !config.providers.is_empty() => Ok(config),
|
||||||
_ => {
|
_ => {
|
||||||
@@ -51,7 +51,7 @@ pub fn load_agent_config_command(agent: Agent) -> Result<Config, String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn save_agent_config_command(agent: Agent, config: Config) -> Result<(), String> {
|
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)
|
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 {
|
fn build_active_config(config: &Config) -> Config {
|
||||||
// Only the provider explicitly marked as active is written to the agent's
|
// 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.
|
// instead of falling back to a managed/native provider.
|
||||||
let providers: IndexMap<String, Provider> = config
|
let providers: IndexMap<String, Provider> = config
|
||||||
.providers
|
.providers
|
||||||
|
|||||||
@@ -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
|
//! 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
|
//! from the agent-specific config files that are written when the user activates
|
||||||
//! a provider.
|
//! a provider.
|
||||||
@@ -16,17 +16,43 @@ use crate::models::{Agent, Config, Model, Provider, ProviderType};
|
|||||||
|
|
||||||
pub type DbResult<T> = anyhow::Result<T>;
|
pub type DbResult<T> = anyhow::Result<T>;
|
||||||
|
|
||||||
pub fn pi_switch_data_dir() -> PathBuf {
|
pub fn kimi_switch_data_dir() -> PathBuf {
|
||||||
dirs::home_dir()
|
dirs::home_dir()
|
||||||
.map(|h| h.join(".pi-switch"))
|
.map(|h| h.join(".kimi-switch"))
|
||||||
.expect("failed to resolve home directory")
|
.expect("failed to resolve home directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn db_path() -> PathBuf {
|
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<Connection> {
|
pub fn init_db() -> DbResult<Connection> {
|
||||||
|
migrate_legacy_data_dir();
|
||||||
let path = db_path();
|
let path = db_path();
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent)
|
std::fs::create_dir_all(parent)
|
||||||
@@ -64,7 +90,6 @@ pub fn init_db() -> DbResult<Connection> {
|
|||||||
model TEXT NOT NULL,
|
model TEXT NOT NULL,
|
||||||
max_context_size INTEGER NOT NULL,
|
max_context_size INTEGER NOT NULL,
|
||||||
display_name TEXT,
|
display_name TEXT,
|
||||||
role TEXT,
|
|
||||||
supports_1m INTEGER NOT NULL DEFAULT 0,
|
supports_1m INTEGER NOT NULL DEFAULT 0,
|
||||||
capabilities TEXT,
|
capabilities TEXT,
|
||||||
raw_other TEXT,
|
raw_other TEXT,
|
||||||
@@ -128,20 +153,19 @@ pub fn load_config(agent: &Agent) -> DbResult<Config> {
|
|||||||
let mut models = IndexMap::new();
|
let mut models = IndexMap::new();
|
||||||
{
|
{
|
||||||
let mut stmt = tx.prepare(
|
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",
|
FROM models WHERE agent = ?1 ORDER BY id",
|
||||||
)?;
|
)?;
|
||||||
let model_rows = stmt.query_map(params![agent.as_str()], |row| {
|
let model_rows = stmt.query_map(params![agent.as_str()], |row| {
|
||||||
let caps_json: Option<String> = row.get(7)?;
|
let caps_json: Option<String> = row.get(6)?;
|
||||||
let raw_json: Option<String> = row.get(8)?;
|
let raw_json: Option<String> = row.get(7)?;
|
||||||
Ok(Model {
|
Ok(Model {
|
||||||
alias: row.get(0)?,
|
alias: row.get(0)?,
|
||||||
provider: row.get(1)?,
|
provider: row.get(1)?,
|
||||||
model: row.get(2)?,
|
model: row.get(2)?,
|
||||||
max_context_size: row.get::<_, i64>(3)? as u64,
|
max_context_size: row.get::<_, i64>(3)? as u64,
|
||||||
display_name: row.get(4)?,
|
display_name: row.get(4)?,
|
||||||
role: row.get(5)?,
|
supports_1m: row.get::<_, i32>(5)? != 0,
|
||||||
supports_1m: row.get::<_, i32>(6)? != 0,
|
|
||||||
capabilities: caps_json
|
capabilities: caps_json
|
||||||
.and_then(|s| serde_json::from_str(&s).ok())
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
@@ -202,8 +226,8 @@ pub fn save_config(agent: &Agent, config: &Config) -> DbResult<()> {
|
|||||||
{
|
{
|
||||||
let mut insert_model = tx.prepare(
|
let mut insert_model = tx.prepare(
|
||||||
"INSERT INTO models
|
"INSERT INTO models
|
||||||
(agent, alias, provider_name, model, max_context_size, display_name, role, supports_1m, capabilities, raw_other)
|
(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, ?10)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for model in config.models.values() {
|
for model in config.models.values() {
|
||||||
@@ -214,7 +238,6 @@ pub fn save_config(agent: &Agent, config: &Config) -> DbResult<()> {
|
|||||||
model.model,
|
model.model,
|
||||||
model.max_context_size as i64,
|
model.max_context_size as i64,
|
||||||
model.display_name,
|
model.display_name,
|
||||||
model.role,
|
|
||||||
model.supports_1m as i32,
|
model.supports_1m as i32,
|
||||||
serde_json::to_string(&model.capabilities).ok(),
|
serde_json::to_string(&model.capabilities).ok(),
|
||||||
serde_json::to_string(&model.raw_other).ok(),
|
serde_json::to_string(&model.raw_other).ok(),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Kimi Code stores its configuration in `~/.kimi-code/config.toml`.
|
//! Kimi Code stores its configuration in `~/.kimi-code/config.toml`.
|
||||||
//! This module reads/writes that file and converts between Kimi's 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;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ pub fn save_kimi_code_config(value: &TomlValue) -> KimiResult<()> {
|
|||||||
Ok(())
|
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 {
|
pub fn kimi_code_to_config(value: &TomlValue) -> Config {
|
||||||
let root = value.as_table().cloned().unwrap_or_default();
|
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 providers = IndexMap::new();
|
||||||
let mut models = 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<String> = 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()) {
|
if let Some(providers_table) = root.get("providers").and_then(|v| v.as_table()) {
|
||||||
for (name, pv) in providers_table {
|
for (name, pv) in providers_table {
|
||||||
let table = pv.as_table().cloned().unwrap_or_default();
|
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,
|
official_url: None,
|
||||||
managed,
|
managed,
|
||||||
enabled,
|
enabled,
|
||||||
active: true,
|
active: active_provider_name == Some(name.as_str()),
|
||||||
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<String> = 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,
|
|
||||||
raw_other,
|
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
|
/// If `existing` is provided, unknown top-level sections (e.g. `services`) are
|
||||||
/// preserved; otherwise a fresh TOML table is used.
|
/// 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();
|
let mut providers_table = Table::new();
|
||||||
for (name, provider) in &config.providers {
|
for (name, provider) in &config.providers {
|
||||||
// Only write the active provider to Kimi Code config so the agent
|
// 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 {
|
if !provider.active {
|
||||||
continue;
|
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()) {
|
if let Some(display_name) = model.display_name.clone().filter(|s| !s.is_empty()) {
|
||||||
mt.insert("display_name".to_string(), TomlValue::String(display_name));
|
mt.insert("display_name".to_string(), TomlValue::String(display_name));
|
||||||
}
|
}
|
||||||
let mut capabilities = model.capabilities.clone();
|
let capabilities = model.capabilities.clone();
|
||||||
if model.supports_1m && !capabilities.iter().any(|c| c == "thinking") {
|
|
||||||
capabilities.push("thinking".to_string());
|
|
||||||
}
|
|
||||||
if !capabilities.is_empty() {
|
if !capabilities.is_empty() {
|
||||||
mt.insert(
|
mt.insert(
|
||||||
"capabilities".to_string(),
|
"capabilities".to_string(),
|
||||||
@@ -461,7 +463,8 @@ api_key = ""
|
|||||||
assert_eq!(model.model, "kimi-for-coding");
|
assert_eq!(model.model, "kimi-for-coding");
|
||||||
assert_eq!(model.max_context_size, 262144);
|
assert_eq!(model.max_context_size, 262144);
|
||||||
assert!(model.capabilities.contains(&"thinking".to_string()));
|
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.
|
// Unknown top-level sections are preserved.
|
||||||
let services = config.raw_other.get("services");
|
let services = config.raw_other.get("services");
|
||||||
@@ -496,7 +499,6 @@ api_key = ""
|
|||||||
model: "glm-5.2".to_string(),
|
model: "glm-5.2".to_string(),
|
||||||
max_context_size: 900_000,
|
max_context_size: 900_000,
|
||||||
display_name: None,
|
display_name: None,
|
||||||
role: None,
|
|
||||||
supports_1m: true,
|
supports_1m: true,
|
||||||
capabilities: vec!["thinking".to_string()],
|
capabilities: vec!["thinking".to_string()],
|
||||||
raw_other: Value::Null,
|
raw_other: Value::Null,
|
||||||
@@ -527,4 +529,62 @@ api_key = ""
|
|||||||
let caps = model.get("capabilities").unwrap().as_array().unwrap();
|
let caps = model.get("capabilities").unwrap().as_array().unwrap();
|
||||||
assert!(caps.iter().any(|v| v.as_str() == Some("thinking")));
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,82 @@ pub mod kimi_code_io;
|
|||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod pi_io;
|
pub mod pi_io;
|
||||||
|
|
||||||
|
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||||
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder};
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.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![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::load_agent_config_command,
|
commands::load_agent_config_command,
|
||||||
commands::save_agent_config_command,
|
commands::save_agent_config_command,
|
||||||
|
|||||||
@@ -129,8 +129,6 @@ pub struct Model {
|
|||||||
pub model: String,
|
pub model: String,
|
||||||
pub max_context_size: u64,
|
pub max_context_size: u64,
|
||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub role: Option<String>,
|
|
||||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
pub supports_1m: bool,
|
pub supports_1m: bool,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
@@ -147,7 +145,6 @@ impl std::fmt::Debug for Model {
|
|||||||
.field("model", &self.model)
|
.field("model", &self.model)
|
||||||
.field("max_context_size", &self.max_context_size)
|
.field("max_context_size", &self.max_context_size)
|
||||||
.field("display_name", &self.display_name)
|
.field("display_name", &self.display_name)
|
||||||
.field("role", &self.role)
|
|
||||||
.field("supports_1m", &self.supports_1m)
|
.field("supports_1m", &self.supports_1m)
|
||||||
.field("capabilities", &self.capabilities)
|
.field("capabilities", &self.capabilities)
|
||||||
.field("raw_other", &"<json>")
|
.field("raw_other", &"<json>")
|
||||||
@@ -162,7 +159,6 @@ impl PartialEq for Model {
|
|||||||
&& self.model == other.model
|
&& self.model == other.model
|
||||||
&& self.max_context_size == other.max_context_size
|
&& self.max_context_size == other.max_context_size
|
||||||
&& self.display_name == other.display_name
|
&& self.display_name == other.display_name
|
||||||
&& self.role == other.role
|
|
||||||
&& self.supports_1m == other.supports_1m
|
&& self.supports_1m == other.supports_1m
|
||||||
&& self.capabilities == other.capabilities
|
&& self.capabilities == other.capabilities
|
||||||
&& self.raw_other == other.raw_other
|
&& self.raw_other == other.raw_other
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Pi stores custom providers and models in `~/.pi/agent/models.json`.
|
//! Pi stores custom providers and models in `~/.pi/agent/models.json`.
|
||||||
//! This module reads and writes that file and converts between Pi's 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;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ pub fn pi_settings_path() -> PathBuf {
|
|||||||
pi_agent_dir().join("settings.json")
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct PiSettingsFile {
|
pub struct PiSettingsFile {
|
||||||
#[serde(rename = "defaultProvider", default, skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "defaultProvider", default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -197,7 +197,7 @@ pub fn save_pi_settings(file: &PiSettingsFile) -> PiResult<()> {
|
|||||||
Ok(())
|
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 {
|
pub fn pi_api_for_provider(provider_type: &ProviderType) -> &'static str {
|
||||||
match provider_type {
|
match provider_type {
|
||||||
ProviderType::Openai => "openai-completions",
|
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 {
|
pub fn provider_type_for_pi_api(api: &str) -> ProviderType {
|
||||||
match api {
|
match api {
|
||||||
"openai-responses" => ProviderType::OpenaiResponses,
|
"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
|
/// 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`.
|
/// `Config`.
|
||||||
fn merge_provider_known_into_raw(pi_provider: &PiProvider, raw: &mut Value) {
|
fn merge_provider_known_into_raw(pi_provider: &PiProvider, raw: &mut Value) {
|
||||||
let has_known = pi_provider.headers.is_some()
|
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
|
/// 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`.
|
/// `Config`.
|
||||||
fn merge_model_known_into_raw(pi_model: &PiModel, raw: &mut Value) {
|
fn merge_model_known_into_raw(pi_model: &PiModel, raw: &mut Value) {
|
||||||
let has_known = pi_model.cost.is_some()
|
let has_known = pi_model.cost.is_some()
|
||||||
@@ -310,7 +310,7 @@ fn extract_model_fields(raw: &Value) -> (Option<PiCost>, Option<Value>, Option<u
|
|||||||
(cost, compat, max_tokens, extra)
|
(cost, compat, max_tokens, extra)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Import a Pi `models.json` into a Pi Switch `Config`.
|
/// Import a Pi `models.json` into a Kimi Switch `Config`.
|
||||||
pub fn pi_file_to_config(file: &PiModelsFile) -> Config {
|
pub fn pi_file_to_config(file: &PiModelsFile) -> Config {
|
||||||
let mut providers = IndexMap::new();
|
let mut providers = IndexMap::new();
|
||||||
let mut models = 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(),
|
model: pi_model.id.clone(),
|
||||||
max_context_size: pi_model.context_window,
|
max_context_size: pi_model.context_window,
|
||||||
display_name,
|
display_name,
|
||||||
role: None,
|
|
||||||
supports_1m: pi_model.reasoning || pi_model.context_window >= 1_000_000,
|
supports_1m: pi_model.reasoning || pi_model.context_window >= 1_000_000,
|
||||||
capabilities: vec![],
|
capabilities: vec![],
|
||||||
raw_other: model_raw,
|
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 {
|
pub fn config_to_pi_file(config: &Config) -> PiModelsFile {
|
||||||
let mut providers = IndexMap::new();
|
let mut providers = IndexMap::new();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"productName": "Pi Switch",
|
"productName": "Kimi Switch",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"identifier": "com.piswitch.app",
|
"identifier": "com.kimiswitch.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
"beforeBuildCommand": "npm run build",
|
"beforeBuildCommand": "npm run build",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Pi Switch",
|
"title": "Kimi Switch",
|
||||||
"width": 1200,
|
"width": 1200,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 1000,
|
"minWidth": 1000,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useTranslation } from "./i18n";
|
|||||||
import { getDefaultMaxContextSize } from "./lib/model-defaults";
|
import { getDefaultMaxContextSize } from "./lib/model-defaults";
|
||||||
import type { Agent, Model, Provider } from "./types";
|
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 {
|
function getProviderDefaultModel(provider: Provider): string | null {
|
||||||
const raw = provider.raw_other as Record<string, unknown> | undefined;
|
const raw = provider.raw_other as Record<string, unknown> | undefined;
|
||||||
@@ -65,6 +65,7 @@ export default function App() {
|
|||||||
const [view, setView] = useState<"list" | "edit">("list");
|
const [view, setView] = useState<"list" | "edit">("list");
|
||||||
const [editingProvider, setEditingProvider] = useState<string>("");
|
const [editingProvider, setEditingProvider] = useState<string>("");
|
||||||
const [loadTimeout, setLoadTimeout] = useState(false);
|
const [loadTimeout, setLoadTimeout] = useState(false);
|
||||||
|
const [switchMessage, setSwitchMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const setAgent = (next: Agent) => {
|
const setAgent = (next: Agent) => {
|
||||||
setAgentState(next);
|
setAgentState(next);
|
||||||
@@ -243,7 +244,7 @@ export default function App() {
|
|||||||
|
|
||||||
// Only the selected provider is active. All other providers (including
|
// Only the selected provider is active. All other providers (including
|
||||||
// Kimi native/managed and custom) are deactivated so the target agent
|
// Kimi native/managed and custom) are deactivated so the target agent
|
||||||
// follows Pi Switch's choice exactly.
|
// follows Kimi Switch's choice exactly.
|
||||||
const providers: Record<string, Provider> = {};
|
const providers: Record<string, Provider> = {};
|
||||||
for (const [key, p] of Object.entries(cfg.providers)) {
|
for (const [key, p] of Object.entries(cfg.providers)) {
|
||||||
providers[key] = { ...p, active: key === name };
|
providers[key] = { ...p, active: key === name };
|
||||||
@@ -271,9 +272,19 @@ export default function App() {
|
|||||||
return { ...cfg, providers, default_model };
|
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 save();
|
||||||
await invoke("activate_agent_config_command", { agent });
|
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[]) => {
|
const handleApplyProviderJson = (provider: Provider, models: Model[]) => {
|
||||||
@@ -391,6 +402,12 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{switchMessage && (
|
||||||
|
<div className="bg-green-900/20 text-green-400 p-2 text-sm border-b border-green-500/20 text-center">
|
||||||
|
{switchMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<main className="flex-1 overflow-hidden relative">
|
<main className="flex-1 overflow-hidden relative">
|
||||||
{view === "list" ? (
|
{view === "list" ? (
|
||||||
<ProviderList
|
<ProviderList
|
||||||
@@ -452,11 +469,6 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onModelAdd={() => {
|
onModelAdd={() => {
|
||||||
const providerModels = Object.values(config.models).filter(
|
|
||||||
(m) => m.provider === currentProvider.name
|
|
||||||
);
|
|
||||||
const roles = ["Sonnet", "Opus", "Fable", "Haiku"];
|
|
||||||
const defaultRole = roles[providerModels.length % roles.length];
|
|
||||||
const alias = `新模型[${currentProvider.name}]`;
|
const alias = `新模型[${currentProvider.name}]`;
|
||||||
updateConfig((cfg) => ({
|
updateConfig((cfg) => ({
|
||||||
...cfg,
|
...cfg,
|
||||||
@@ -468,9 +480,8 @@ export default function App() {
|
|||||||
model: "",
|
model: "",
|
||||||
max_context_size: getDefaultMaxContextSize(alias),
|
max_context_size: getDefaultMaxContextSize(alias),
|
||||||
display_name: null,
|
display_name: null,
|
||||||
role: defaultRole,
|
|
||||||
supports_1m: false,
|
supports_1m: false,
|
||||||
capabilities: [],
|
capabilities: agent === "kimi_code" ? ["thinking"] : [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,10 +1,30 @@
|
|||||||
import { useEffect, useId, useState } from "react";
|
import { useEffect, useId, useState } from "react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useTranslation } from "../i18n";
|
import { useTranslation } from "../i18n";
|
||||||
|
import type { TranslationKey } from "../i18n/zh";
|
||||||
import { getDefaultMaxContextSize } from "../lib/model-defaults";
|
import { getDefaultMaxContextSize } from "../lib/model-defaults";
|
||||||
import { AgentSettingsPanel } from "./AgentSettingsPanel";
|
import { AgentSettingsPanel } from "./AgentSettingsPanel";
|
||||||
import type { Agent, DiscoveredModel, Model, Provider, ProviderType } from "../types";
|
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[] = [
|
const PROVIDER_TYPES: ProviderType[] = [
|
||||||
"openai",
|
"openai",
|
||||||
"openai_responses",
|
"openai_responses",
|
||||||
@@ -346,6 +366,7 @@ function ModelMapping({
|
|||||||
const [discovered, setDiscovered] = useState<DiscoveredModel[] | null>(null);
|
const [discovered, setDiscovered] = useState<DiscoveredModel[] | null>(null);
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
const [discoverError, setDiscoverError] = useState<string | null>(null);
|
const [discoverError, setDiscoverError] = useState<string | null>(null);
|
||||||
|
const [fetchThinking, setFetchThinking] = useState(true);
|
||||||
|
|
||||||
const handleDiscover = async () => {
|
const handleDiscover = async () => {
|
||||||
setDiscovering(true);
|
setDiscovering(true);
|
||||||
@@ -379,7 +400,6 @@ function ModelMapping({
|
|||||||
for (const dm of discovered) {
|
for (const dm of discovered) {
|
||||||
if (!selected.has(dm.id)) continue;
|
if (!selected.has(dm.id)) continue;
|
||||||
const alias = dm.id.replace(/[^a-zA-Z0-9_-]/g, "-");
|
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);
|
const max_context_size = dm.max_context_size ?? getDefaultMaxContextSize(dm.id);
|
||||||
toAdd.push({
|
toAdd.push({
|
||||||
alias,
|
alias,
|
||||||
@@ -387,9 +407,8 @@ function ModelMapping({
|
|||||||
model: dm.id,
|
model: dm.id,
|
||||||
max_context_size,
|
max_context_size,
|
||||||
display_name: dm.display_name,
|
display_name: dm.display_name,
|
||||||
role,
|
|
||||||
supports_1m: max_context_size >= 1_000_000,
|
supports_1m: max_context_size >= 1_000_000,
|
||||||
capabilities: [],
|
capabilities: fetchThinking ? ["thinking"] : [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
onBulkAdd(toAdd);
|
onBulkAdd(toAdd);
|
||||||
@@ -434,6 +453,15 @@ function ModelMapping({
|
|||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">
|
||||||
{t("discoveredModels", { count: discovered.length })}
|
{t("discoveredModels", { count: discovered.length })}
|
||||||
</div>
|
</div>
|
||||||
|
<label className="flex items-center gap-2 text-sm text-[#e5e5e7] cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={fetchThinking}
|
||||||
|
onChange={(e) => setFetchThinking(e.target.checked)}
|
||||||
|
className="w-4 h-4 rounded border-[#2a2a2e] bg-[#1f1f23] text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{t("fetchEnableThinking")}
|
||||||
|
</label>
|
||||||
<div className="max-h-48 overflow-auto space-y-1">
|
<div className="max-h-48 overflow-auto space-y-1">
|
||||||
{discovered.map((m) => (
|
{discovered.map((m) => (
|
||||||
<label
|
<label
|
||||||
@@ -467,7 +495,6 @@ function ModelMapping({
|
|||||||
<table className="w-full min-w-[640px] text-sm">
|
<table className="w-full min-w-[640px] text-sm">
|
||||||
<thead className="bg-[#1f1f23] text-gray-400">
|
<thead className="bg-[#1f1f23] text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left px-4 py-3 font-medium">{t("modelRole")}</th>
|
|
||||||
<th className="text-left px-4 py-3 font-medium">{t("displayName")}</th>
|
<th className="text-left px-4 py-3 font-medium">{t("displayName")}</th>
|
||||||
<th className="text-left px-4 py-3 font-medium">{t("actualModel")}</th>
|
<th className="text-left px-4 py-3 font-medium">{t("actualModel")}</th>
|
||||||
<th className="text-left px-4 py-3 font-medium w-28">{t("contextSize")}</th>
|
<th className="text-left px-4 py-3 font-medium w-28">{t("contextSize")}</th>
|
||||||
@@ -482,18 +509,6 @@ function ModelMapping({
|
|||||||
<tbody className="divide-y divide-[#2a2a2e]">
|
<tbody className="divide-y divide-[#2a2a2e]">
|
||||||
{models.map((m) => (
|
{models.map((m) => (
|
||||||
<tr key={m.alias} className="hover:bg-[#1c1c20]">
|
<tr key={m.alias} className="hover:bg-[#1c1c20]">
|
||||||
<td className="px-4 py-2">
|
|
||||||
<select
|
|
||||||
className="w-full bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
||||||
value={m.role || "Sonnet"}
|
|
||||||
onChange={(e) => onModelChange({ ...m, role: e.target.value })}
|
|
||||||
>
|
|
||||||
<option value="Sonnet">Sonnet</option>
|
|
||||||
<option value="Opus">Opus</option>
|
|
||||||
<option value="Fable">Fable</option>
|
|
||||||
<option value="Haiku">Haiku</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<span className="text-sm text-gray-400">
|
<span className="text-sm text-gray-400">
|
||||||
{m.model ? `${m.model}[${m.provider}]` : m.alias}
|
{m.model ? `${m.model}[${m.provider}]` : m.alias}
|
||||||
@@ -539,18 +554,10 @@ function ModelMapping({
|
|||||||
</td>
|
</td>
|
||||||
{agent === "kimi_code" && (
|
{agent === "kimi_code" && (
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<input
|
<CapabilitiesCell
|
||||||
className="w-full bg-transparent border border-[#2a2a2e] rounded px-2 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
capabilities={m.capabilities || []}
|
||||||
value={(m.capabilities || []).join(", ")}
|
onChange={(next) =>
|
||||||
placeholder="thinking, image_in"
|
onModelChange({ ...m, capabilities: next })
|
||||||
onChange={(e) =>
|
|
||||||
onModelChange({
|
|
||||||
...m,
|
|
||||||
capabilities: e.target.value
|
|
||||||
.split(",")
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@@ -668,3 +675,55 @@ function JsonPreview({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CapabilitiesCell({
|
||||||
|
capabilities,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
capabilities: string[];
|
||||||
|
onChange: (next: string[]) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const known = new Set<string>(KNOWN_CAPABILITIES);
|
||||||
|
const customs = capabilities.filter((c) => !known.has(c));
|
||||||
|
|
||||||
|
const toggle = (cap: string, on: boolean) => {
|
||||||
|
if (on) {
|
||||||
|
onChange([...capabilities.filter((c) => c !== cap), cap]);
|
||||||
|
} else {
|
||||||
|
onChange(capabilities.filter((c) => c !== cap));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
|
||||||
|
{KNOWN_CAPABILITIES.map((cap) => (
|
||||||
|
<label
|
||||||
|
key={cap}
|
||||||
|
className="flex items-center gap-1 text-xs text-gray-400 cursor-pointer"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={capabilities.includes(cap)}
|
||||||
|
onChange={(e) => toggle(cap, e.target.checked)}
|
||||||
|
className="w-3.5 h-3.5 rounded border-[#2a2a2e] bg-[#1f1f23] text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{t(CAPABILITY_LABELS[cap])}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
className="w-28 bg-transparent border border-[#2a2a2e] rounded px-1.5 py-1 text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
|
value={customs.join(", ")}
|
||||||
|
placeholder={t("customCapabilities")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newCustoms = e.target.value
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const kept = capabilities.filter((c) => known.has(c));
|
||||||
|
onChange([...kept, ...newCustoms]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -66,9 +66,21 @@ export function ProviderList({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onAdd}
|
onClick={onAdd}
|
||||||
title={t("addProvider")}
|
title={t("addProvider")}
|
||||||
className="w-9 h-9 flex items-center justify-center rounded-full bg-orange-500 hover:bg-orange-600 text-white text-xl shadow-lg focus:ring-2 focus:ring-orange-400 focus:outline-none transition-colors"
|
className="w-9 h-9 flex items-center justify-center rounded-full bg-orange-500 hover:bg-orange-600 text-white shadow-lg focus:ring-2 focus:ring-orange-400 focus:outline-none transition-colors"
|
||||||
>
|
>
|
||||||
+
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M12 5v14M5 12h14" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -80,9 +92,21 @@ export function ProviderList({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onAdd}
|
onClick={onAdd}
|
||||||
className="mt-4 w-12 h-12 rounded-full bg-orange-500 hover:bg-orange-600 text-white text-2xl shadow-lg flex items-center justify-center mx-auto"
|
className="mt-4 w-12 h-12 rounded-full bg-orange-500 hover:bg-orange-600 text-white shadow-lg flex items-center justify-center mx-auto"
|
||||||
>
|
>
|
||||||
+
|
<svg
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M12 5v14M5 12h14" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { TranslationKey } from "./zh";
|
|||||||
|
|
||||||
export const enTranslations: Record<TranslationKey, string> = {
|
export const enTranslations: Record<TranslationKey, string> = {
|
||||||
// App / global
|
// App / global
|
||||||
appTitle: "Pi Switch",
|
appTitle: "Kimi Switch",
|
||||||
loading: "Loading...",
|
loading: "Loading...",
|
||||||
loadTimeout: "Loading is taking a while. Try restarting the app if it stays stuck.",
|
loadTimeout: "Loading is taking a while. Try restarting the app if it stays stuck.",
|
||||||
loadFailed: "Failed to load:",
|
loadFailed: "Failed to load:",
|
||||||
@@ -58,7 +58,6 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
fetchingModels: "Fetching...",
|
fetchingModels: "Fetching...",
|
||||||
discoveredModels: "Discovered {count} models",
|
discoveredModels: "Discovered {count} models",
|
||||||
addSelected: "Add selected",
|
addSelected: "Add selected",
|
||||||
modelRole: "Model role",
|
|
||||||
displayName: "Display name",
|
displayName: "Display name",
|
||||||
actualModel: "Actual model",
|
actualModel: "Actual model",
|
||||||
contextSize: "Context size",
|
contextSize: "Context size",
|
||||||
@@ -88,6 +87,7 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
switchTo: "Switch to",
|
switchTo: "Switch to",
|
||||||
inUse: "In use",
|
inUse: "In use",
|
||||||
switchReloadHint: "After switching providers, run /reload in Kimi Code to apply the change.",
|
switchReloadHint: "After switching providers, run /reload in Kimi Code to apply the change.",
|
||||||
|
reloadCopiedHint: "Provider switched. /reload copied to clipboard; paste it in Kimi Code.",
|
||||||
unsavedChanges: "Unsaved changes",
|
unsavedChanges: "Unsaved changes",
|
||||||
qwenActivateNotImplemented: "Qwen Code activation will be implemented in a follow-up.",
|
qwenActivateNotImplemented: "Qwen Code activation will be implemented in a follow-up.",
|
||||||
agentSettings: "Global Settings",
|
agentSettings: "Global Settings",
|
||||||
@@ -123,6 +123,13 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
importConfigConfirm: "Importing will overwrite unsaved changes. Continue?",
|
importConfigConfirm: "Importing will overwrite unsaved changes. Continue?",
|
||||||
importConfigFailed: "Import failed: {message}",
|
importConfigFailed: "Import failed: {message}",
|
||||||
capabilities: "Capabilities",
|
capabilities: "Capabilities",
|
||||||
|
fetchEnableThinking: "Enable thinking for fetched models",
|
||||||
|
capThinking: "Thinking",
|
||||||
|
capAlwaysThinking: "Always Thinking",
|
||||||
|
capImageIn: "Image Input",
|
||||||
|
capVideoIn: "Video Input",
|
||||||
|
capToolUse: "Tool Use",
|
||||||
|
customCapabilities: "Other capabilities (comma-separated)",
|
||||||
|
|
||||||
// Languages
|
// Languages
|
||||||
language: "Language",
|
language: "Language",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ interface I18nContextValue {
|
|||||||
|
|
||||||
const I18nContext = createContext<I18nContextValue | null>(null);
|
const I18nContext = createContext<I18nContextValue | null>(null);
|
||||||
|
|
||||||
const STORAGE_KEY = "pi-switch-lang";
|
const STORAGE_KEY = "kimi-switch-lang";
|
||||||
|
|
||||||
function getInitialLang(): Language {
|
function getInitialLang(): Language {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const zhTranslations = {
|
export const zhTranslations = {
|
||||||
// App / global
|
// App / global
|
||||||
appTitle: "Pi Switch",
|
appTitle: "Kimi Switch",
|
||||||
loading: "加载中...",
|
loading: "加载中...",
|
||||||
loadTimeout: "加载时间较长,如果一直卡住,请尝试重新启动应用。",
|
loadTimeout: "加载时间较长,如果一直卡住,请尝试重新启动应用。",
|
||||||
loadFailed: "加载失败:",
|
loadFailed: "加载失败:",
|
||||||
@@ -56,7 +56,6 @@ export const zhTranslations = {
|
|||||||
fetchingModels: "获取中...",
|
fetchingModels: "获取中...",
|
||||||
discoveredModels: "发现 {count} 个模型",
|
discoveredModels: "发现 {count} 个模型",
|
||||||
addSelected: "添加选中的",
|
addSelected: "添加选中的",
|
||||||
modelRole: "模型角色",
|
|
||||||
displayName: "显示名称",
|
displayName: "显示名称",
|
||||||
actualModel: "实际请求模型",
|
actualModel: "实际请求模型",
|
||||||
contextSize: "上下文长度",
|
contextSize: "上下文长度",
|
||||||
@@ -86,6 +85,7 @@ export const zhTranslations = {
|
|||||||
switchTo: "切换使用",
|
switchTo: "切换使用",
|
||||||
inUse: "使用中",
|
inUse: "使用中",
|
||||||
switchReloadHint: "切换供应商后,请在 Kimi Code 中执行 /reload 以生效。",
|
switchReloadHint: "切换供应商后,请在 Kimi Code 中执行 /reload 以生效。",
|
||||||
|
reloadCopiedHint: "已切换供应商,/reload 已复制到剪贴板,请在 Kimi Code 中粘贴执行。",
|
||||||
unsavedChanges: "未保存修改",
|
unsavedChanges: "未保存修改",
|
||||||
qwenActivateNotImplemented: "Qwen Code 启用功能将在后续实现。",
|
qwenActivateNotImplemented: "Qwen Code 启用功能将在后续实现。",
|
||||||
agentSettings: "全局配置",
|
agentSettings: "全局配置",
|
||||||
@@ -121,6 +121,13 @@ export const zhTranslations = {
|
|||||||
importConfigConfirm: "导入会覆盖当前未保存的修改,是否继续?",
|
importConfigConfirm: "导入会覆盖当前未保存的修改,是否继续?",
|
||||||
importConfigFailed: "导入失败:{message}",
|
importConfigFailed: "导入失败:{message}",
|
||||||
capabilities: "能力标签",
|
capabilities: "能力标签",
|
||||||
|
fetchEnableThinking: "为获取的模型启用思考",
|
||||||
|
capThinking: "思考",
|
||||||
|
capAlwaysThinking: "始终思考",
|
||||||
|
capImageIn: "图像输入",
|
||||||
|
capVideoIn: "视频输入",
|
||||||
|
capToolUse: "工具调用",
|
||||||
|
customCapabilities: "其他能力(逗号分隔)",
|
||||||
|
|
||||||
// Languages
|
// Languages
|
||||||
language: "语言",
|
language: "语言",
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ export interface Model {
|
|||||||
model: string;
|
model: string;
|
||||||
max_context_size: number;
|
max_context_size: number;
|
||||||
display_name: string | null;
|
display_name: string | null;
|
||||||
/** Optional model role, e.g. Sonnet/Opus/Fable/Haiku for Claude-style mapping. */
|
|
||||||
role?: string | null;
|
|
||||||
/** Whether the model declares extended context / thinking support. */
|
/** Whether the model declares extended context / thinking support. */
|
||||||
supports_1m?: boolean;
|
supports_1m?: boolean;
|
||||||
/** Agent-specific capability flags (e.g. Kimi Code `capabilities`). */
|
/** Agent-specific capability flags (e.g. Kimi Code `capabilities`). */
|
||||||
|
|||||||