From 948e96352eb6b901c61b01c66aa332f9367539e0 Mon Sep 17 00:00:00 2001 From: KimiSwitch Dev Date: Sat, 25 Jul 2026 16:39:20 +0800 Subject: [PATCH] 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 --- README.md | 28 +-- README_EN.md | 28 +-- .../plans/2026-07-07-i18n-ui-agent-switch.md | 8 +- ...9-kimicode-global-settings-panel-design.md | 2 +- index.html | 4 +- package-lock.json | 4 +- package.json | 4 +- public/kimi.svg | 13 ++ public/pi.svg | 10 - scripts/generate-icons.py | 124 ++++++----- src-tauri/Cargo.toml | 6 +- src-tauri/icons/128x128.png | Bin 1640 -> 6621 bytes src-tauri/icons/128x128@2x.png | Bin 3172 -> 15225 bytes src-tauri/icons/32x32.png | Bin 484 -> 1416 bytes src-tauri/icons/icon.ico | Bin 506 -> 1438 bytes src-tauri/icons/icon.png | Bin 1592 -> 15225 bytes src-tauri/src/commands.rs | 6 +- src-tauri/src/db.rs | 51 +++-- src-tauri/src/kimi_code_io.rs | 204 +++++++++++------- src-tauri/src/lib.rs | 73 +++++++ src-tauri/src/models.rs | 4 - src-tauri/src/pi_io.rs | 17 +- src-tauri/tauri.conf.json | 8 +- src/App.tsx | 31 ++- src/components/ProviderEdit.tsx | 115 +++++++--- src/components/ProviderList.tsx | 32 ++- src/i18n/en.ts | 11 +- src/i18n/index.tsx | 2 +- src/i18n/zh.ts | 11 +- src/types/index.ts | 2 - 30 files changed, 543 insertions(+), 255 deletions(-) create mode 100644 public/kimi.svg delete mode 100644 public/pi.svg 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` `<title>` 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 @@ <html lang="zh-CN"> <head> <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" /> - <title>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 @@ + + + + + + + + + K + + + \ 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 = ''' - + - - π + + K + + ''' 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 5a7548407d5735578548b31acd30d953f338353c..7519dc875bae974fd779649940778462436de56a 100644 GIT binary patch literal 6621 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 b@Ui?q3a+yxe@Cpc00000NkvXXu0mjfqih-X delta 1624 zcmV-e2B-PmGw2MEBYy@jNkl-D@317{;I7^N}VRlt>ku(1r?PF02-Bg!X^% zPC-QRP6e+71@X#%M)X>5ywMAhhN!U=+A38qj8$w%qisGsJ!jX8^rYuwchAo3?9Mwg z&kq(#Hl4FG?>o;k^X~3Rbm8ND1VCEh|3=2HLLlu*LnDtmdViDGrWc9PcDxrh(|7W- zogB)x?F-OAs%DK|mov%Gj-(FfIBs0!1Dr_)=@#Hx;IkO^LF7wk;skz>i{84Qwd@13!(GM6gi_Wblh=)E1gn00zGtjU_=- z3BbUs(}*QBVSfP(el==UgSr)f!8b!~Nl<43F!*MvB?;=X00zGvj3fj0uvU%;M=9r_*b<6244o+N>IfD7<^f1C4sI5 zF!(Z2jth4IfumCipaeghv+2t6^PsCW52j=G@ zbS(BJNq>M>KRB>=0h){|fOz?TdNsxyFLiKsMds4QF*QvAM3TNJvXJv@Exh+;7qQIP z0{~nZ%TWXm=Qt;qBGJKj6=xQKdDr>Zc8Z&SC)hs#F22y3I)Ck#1ix)1N}sj#XoMG^ zn{MmM&k272GrO%Man1q&Jg5XZC0@67QhfPMAAcKH2Dr6zbm>cvM41V&d3AslQ4)E2wT3fkgZ+{pdxjzwy*PqvqsNh^4MZfRTc$LAK z1h{@9!TaC;hy8=289#a5CWB@&nWW>)^8i~p-dhTjeRO2J%HoUy%%TYk@KOPM=}=R? z?SIR74T<5Ms1X&MsRYgooY%to z8V~Uv9AwXj6`^k=Rl%D@V5gJalYT!{p0&4kQuGHvdqP#tJ=?vAGrC)^g`go9PrLtU7UI{22xO8sp;9;>m4P4PJ4I$P4U^M_mJl2z&HZNK;wwK zv4AtH(+RM(l>n*!*6q8E0phramw#XBVs5VZcU>JvFA`vRCB{;Q1^jevP&qIg>jPZB zdWZ`bTX<%zg_Dm*SXhW~c$i{$H^r|v5?sDq{c~&)z5Dk5@ZV*{j`+iv1w>#HghT{* zR0$+{c(Dc``b^#pqz#0@@w}%61P(1AaIziPzC&CDu3MmyctBa z;36<^iV}b};1`*p<{~g~X1y0c74U;xp)a!rU=p}ZVTfq~fm8O?Fsgu?%oKPkK;Wp< z0jL6QQ7W{mg&-z@8 zU|ePEycM W^(+VlfQ7{X00006!TBRga zd!-aXB;Qv*@89*ke&4^iuAK2Y_w$_R+|PZU^UTV^n1%5iBM1ayF}Z&A76?QI{7VI* zhX8+^LOyzdKw5exSM~2a&fCssY8AScPPDWRI~tnLoj0RqP>6uBh`wNZ?agV$eog<< z*JT#SyfaVu+3v~uNo)V-Q&(8{^cm>qnIJne{t^`jRm3we6%FyKD7y&$k^wXOUY3^1 zPiJe&?)QhI`336{3p-KX0V8;(qlF!fpE{EVpF%Ar%cUVY(qdvcHDFwWZM;hzvVS8~ zB6%&2vo-1gCfvFcp$=k>oibMJksW)WxpWs+b?0|vB_`%VZ`kH&Z^1zsOt^Q$1UAIsWEcoy~DUWQ1Ez!=rUhRfnnu zw>aL=l`OOJOJ;VB28T~C%QQUWtD2qfmg&~c;*+XfGX87s%EM+;l(NFiPCSdP%Wv)T zY?VaN3d0?wtl&~1v&}LHKAsX8B2CA|upAe&4>f5QNk7*vG9bi+<(1I_3KSPIm8p+a z{hew5E~77gNmKyb??svJ>dox`Cq$gHQ#}<2l*P2X~sL z@Ekn@uqMbzn;UXYRF-u{_d1l4zVHhz^4oxP^_X07Z?Ruj*u`t-Y4gg#Q(MJy7SQMG zwIH`{pL=|i8N}Qd9osfG89B?TTZp~^Pi4EIZ~J;?mP(+6O0lgK)PrwYwTxW&E;Vg6 zeRS=W-#*$<$;pixYjWV}}1Z@Tgomf2~#$&fTHs0}`74VK~-0O4%SCz^dyP|#VYoZ<_VGgQbgx}BW zSA<^jLzd(u_v0Ddwd}c6A6K`(3hkV~{a|*Ik{;3Z@%}Kc0z_3n1oKrD)X{D1OmfSNWSSpj6l&ARV@$_0A@s6^eUpm65 zX$KKR;nRXKF}q-zkI%NmvT7l27#4>oxKZKRXi>hAmbM^^uQuZfyme}{O9-U`Vuyde zv`)xr|F$I`IcG`S0o2dml4m~k*T-ai0e`xaS>5S*!j+!rhU}tNJ-?$n!1?~w*{a$j zo7v((Wh-xbnI&TP&3p6ITvFGPbuf#^Ok#tJz8R=&)D^_r=v3~inm=+Yn~F?y9Wq0rWZy2#+4b$U@cnQfiFF(>8;eX#6R?XI;cIq^9lC^Ps4c~{ zG{l?5w0vy3H8bAJ^=z@c1u);`?`mZ~;cwrcksM@3G#a*K_vc9owocmH%6{OCmslDvq55hP`AX4=D9yW$25&?ly9} zJ)I2N^va`TDFkZIpBvIC4btFV_mGBRECXOH2l^)j+s*q`msc}-;BiPt^^oJEMBA_& ziLqw7*fg$zB~mV1{d<=A#Ah#lm`_U(b30_~NuI~{Ot0G~rIKhk-%dq}sszoo75RHaTm012+^FW{xPxXk z6+WV@_r+8q)=Xj7$}fCJ&rz;>zT3@-@(n#!W+NZ^TK44$7_pyujgYqNosXRNkH=&M znU%oB(=;SES)CmYsr4@YqIwccnGf{S7!~3F(gl^n`RPu~>SdtfUN3)Og{@KlW(~Wm zyX&p2@ku=gMxk5GeRNOjjOt6+Jlg}Z8dFs8VB2>Ee2DXM0y6^{VnE;`jS2MqT)Uig zOICX{J!9J`62Hn6<<37=0V`VXpQOXZWZ86*bz8Rdlg)ysWvV0ucdDMXktNfCZ_+XI6F18vbf1~nm%T;j|!_xwTLk2Scc_IUR zjrsROyqa;=+kyf0YytV{#jdXy!GNczt3NRuRmYq?-$^z8^K5x>y^VYRid-b*=&d1` zI2eXu%a6;`C8aOfX-V8D&rC}x$zE`!#dc(p}BTacjG2i4& z6LZ7HHac#eAGFMXMo#TbKX2TwB;4~MA1RD1pxpE$nsy$uxm`OBZcOF-)jU`EMexC? zYv~DFJ!!oD{2QTK=^%xX7 zylbv9?do}NOLk}8kYLLvcIkLb9kGakdhSWBt3R@dAiu+n@a)7F#Q0sz(%o289j@0| zW$2lal?%>R`th;BNf!WdF_S>2$q%tp_kY|$vlEx;qS?5h4pBE{A34MpzVbzU9r5y# zSyaj8Z6;lqL|6?$jianU|GJK1wRQb{NsN&!!i&DkEs-(6uHTrmKQpP;Y+oABeZ*3Ul&y8?pqRW4=q;g9SD)s z8)>t8UbQY6E0*&_b$%-P-um=ae>NK|$;`>}8u61CgRU(qb;+1)>G zgZ(Tf;oRP5lRO}MvUq}J2C4#nKuywIo97hD^5~O)0mhip_GBsJ6>;QqKW@Gb1*xmV z-Sy&hXj>KdZoi*EGJaNP9#C z_UBI1!QGaFK;mh@59TOp=!?Gt zB&5y#Ljz2j3GN0FL3GO%+s(m+>OrtmObJ_4;f!~99cv5;{SgmqKiSh7;>=~6;Uj}_ zMbkx8R}aS*2c1R8`A4>+)sx1n#wzXEF$#b0e-&FZCVqOQlD5PYly$sR`o@-S?*Wm) z-TTPOg~1q89T2nH=5%7?@j^!t+*&omWE!CTdKjQ)McsL>zEw&Tm{IYI+&k+Wq(2HWJo)?`oR+WqZwV64!@-zAN!Inqmx-|z082>J;V8?rKeUU{Cb5nvBFet4unue^#CfgAc!Ci z@&m;lfO<|mvR*R?QlHmjqRMq8r_KLqxBZBU+V?+oE@GTf^3AHXi-mKGC z3X7~S<3T8dU%s9qzIn6DO+IPr=N;F^ zAH=~btwE-WqKmT85uC)|Ltv40=ora=}2lf?u+7Gb(=B)cTqjh%k)a zhoM&C8|ABcmdKuC<>%HcDvjNPrP#n8>a1T*OqPF+9A3J=uuCHu5{#K%$|3^o;2HRg zvEv2{8%j9&O*U+0KV$`0L=f(cMl-qTvV(L%pk3z5Ha?PJis;UbdR*u!SX148r3DnE z0Q$lVnss8$dMkd_jY70v9VoL+SJgf_9lBt3$q&qYOAcjLgLXX-tPfPGN>QYdOgC!& zXX%?mmP!CkUHd5C-9zHFGYHh!^gP(zPZ`qA9*vKK)vh^$+`OG8_aE!BVdLY|@g32i%}fvu zCkmDXZ@5diYIRkFxDj&b4;erN1jsE@)SP{0?e~6253RR%0DN_UoFdTsg+}G+7$0Gp zeiiLzSHfgvk-4=cZmo9V|2G$XrkA~vci?Vsq561)dSQ})H6c)5!HX6l_?75b@r(3_ zo(&!Vj&-KOy#UpE2gOX#w^rvJBXl!60(z|eFvL=Ec!?e|kXi{6i6CsAQfJ|z=^KVv zFhVe4zGu+%n)fGx4Hde#iFsKXccOm{B62${OtV~QDA6Ewty&^}4W4>|da#f-_65kT z2Can?{{V_b>`WYn%YABmgYUaK^!uI2lf~LPowp~ebD!3&6nB`{(lbFyBxf3Ky|QazOCapG=E(&w`=!!2(nTK=HC0U}uh zL?|y>lD|PT#JfLo8(VV=h)Xd{sg_9^fyspKjx}~ofU->`$2JK`npZx>HwT9zQR{;b z>@xru4j613T3Roe=8ihQ?qG=<)JT`r6{^<~tL=lQ{Yre>71sg6y{+SbaD@@~Uo{3Q z?u)X=grqb*)hi~Q*9KIAkT;7_Y-F<@u&UcjWq%i#LN(tyclMxumuQs?u)HaWNVs*loeFv7d zSGYg_Xh5>I{<`QudGIH`amj6^NKuSd#k+o$UB<}>94Q2U+f&VmS^RsiNftOWOHRH@ zdr>(j&7Ue&Q)SFtfEFh)=pVcDwJ>;sV)~;khpsMn$89_~i_Wwc| zey8wVZ?Fk$aWPDyvrf9flJ<5aHps315;F}17}4;n|F1gf&;Rq)w}*pS zhLCZxQ86F3yLqL`c$}y(g#rQIlZIVXZjRALPU>KtjGy7-n;Hr{gZGomAV(KSc)pK- zG!dcXlCM`&Qm_;dMi5#)G#;q6&b|`aE4+SZJ?uRe-E6MVE>or`vj;wS*e+5>E`Gu? z@yMEmtMJN;)i@ti0M)q26PP9=KpA4}<(LX(5!3{_-gI6isJ>b|Uv~|;{nuo1X=#(x7@(SRb!0*qxQr>YJT#ATD3|y`_ z7Fj#1;jun-DogcYw}YVY@dR-ixQgIwoW!83qm*X#bq0HKy`

O6J5|5f^Y4f0^?oJ^I(ydRlDy=k0p7K2kO8|y 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 literal 3172 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 F{uiXZkT(DT diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index 274362a91cbf8673b6e15b3781320fc6ee038bf1..7b05c84f74b5836b2d6050f8ebe439a46ef105c7 100644 GIT binary patch delta 1398 zcmV-+1&R9P1BeTdBYy=?NklVFqGY1Du*r)7Cl7hlZlQFIg^bxUgli( zWvxHf-m}llnK|R^VX6(Az1M~R``3T@{(lhw?AXC#*Df+Ewtro3=Z4%C0jxt5EhT0m z1_*%tim`7xCnl^_$947#5KEy>!QhJ^tn59qulZt%N$JnSJ)^6X=N(W=b#b5p9gGW~ z19&=C!iAC&CW;1h;J+pgo0x5_J3K6i{m4Gt?>FbT2`PXekVIJ08ROw2~9 zoOGl_P^mon!GFGnK6+@|wKnCszeyoGI1G#c$-2U33(joD_mfEX!?1RwD_HBHh=b)q zI&!PydYv5nd*A{LDf5C&UI>vF!m7ZW#)C9bU_96Bbd20;7dLXKSs*d_+`++YJT!Wh zUmsW=eAYg{{JjnlNs@^Y@jfD*U~W_b)Qu^g+M)<6Hmr48Z(gL2 z3*?BvCY1qWSXgsp0gnLzNJ`$ClP7a*&D_!`{sP3}FoN$mr~tSE;MQ9@UZ7c2%akcv7Qpo zXm$Xsf!jI+S{s2d1kRi{@q82jT^)q^1%f2zOMiP*E(Isve$)H8rGO=Xx$y3L(W29* zV5+m0buYd2ZpjZUj<`H(k;i0*a5QMJ?+Ne%)t8}1~}Uf zYHcnt*lQadRc!@!u^%J+^h<=+W`faz z1b+w2NKS1tydHQ`BQm6#1&!0jz5slm(YcWD+O_R)NlT%ek;=hd(Yb z@;OT^vjeYMAZtlYT`+vv;D_T;tkCUC1b;THbx^#O#~fIFmHz1BC>#|o2>#C zP-~zm51c%q(f3^xE7i7u@cgqb{BlxqQwE)hXT)s!JJ(49(en_eTVtMZ^k|5|L4Ok~ zao_zCYo2i6m&a3gMxJ?Q>x5cx18`dlx)uu*niBp$ch1D~*5nIrBD620>iFClGod5C zG0&(>ovp100yux(#ERJYoI4hf3K+S>7#`v(Y1y@`46EX9HEqXku-W?G3Qp(aoF5BJ z85ly zoft-u#pHe=!M%=@B$)aC08i4H*^|;*zE`M7?+JZ>P)S#iVO^u?e*>;v=?Xva2c^_| z#KHZAVHn=zC`C$olGtc6Q>`&sp}POm>`StX8EmvGJn2eB8oa6JU}0F;3vc3~eGPp< zxwx9Z139lzD(O|TBBskpC!eU?l=V6kZoiK3DDN~%Fb|YV#nn3wHS|?4r|Av(<=3va zdz9Nkz&h5^QZP^3$;`wXwJ}8Oj9t&UQrX*cWc+&m5AZ&lxs$ZFDF6Tf07*qoM6N<$ Ef{%llL;wH) delta 459 zcmV;+0W|)I3*-ZkBYy#_Nkl zU}afweLLXfz~kvT;O>41z}l*$c{IoQWsjDxFvW`nNM&aK)lDCzAlUcleL}k(rgb{u z{O&7!Um@-I&3_%xWeSK_8WpjlJ+l&Ey9)#$R6;DYxyX((cR&kCvRZvE=741@761W6 zc`8T**kS?5NV(~#&BLIt;@^HNTy|sKVQEoVVL1w<{P}h!qvk=qE@-`}_}S}=;^A?~ z-mb^arfZB94beR9!t6Ss{Vph7z<)Up2xjvcM4>)VW<=;UFl`rportC>A~}>6>0V_< zS`Ad~Lhijlf2m9%RyQL(sSFW??}mDE5h1y^<8RHQx1r5s(@_8b002ovPDHLkV1i1% B*p>hQ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index c767b398e25960ba5dee6541159bf8a62104a396..d0b55ea7f2fabfdc71d0c50efdbbf80c8a401297 100644 GIT binary patch delta 1416 zcmV;31$X-T1D*>I000310RSK%00000AOMI3kq{|=1y4ytK~#90y;oa^RaF@N{Lli9~W+Da%fc=WGZ#pL?tX0Q#_6rb8p-#cziy*A*J+iO)Vv0%W z&%-^VtCZ&*P)c=ipaC6>3!ejcI#$Akk`gA026W)Ra(I8!Hx}Ej?ZzJ@+}Xxp_m<+;B}Av-t>i~z~H!e$H3 zY{vJKNcY3AcBCs<>!FB)~jHs zw%~NflIYv*n{$@|ECVp`=~rRzmtXT!juZ|VCL##d0R&OEAU0eTFsFcHmck=P`u*EC zJ3|20hauXc2rD+Mby{y;q>l^ah`=V50b^KLb7TRJ0Rc!#-kOsqb8OAr(kT7{#Nsf5 z?>MLcxB}qTTRP+wqif|`x5s&y37mm{zz0etpi-#?4}G9q1eJ;*hA(mV$W?RJQ7~bM zf>I~|IC<)K-Q@_LD*v&b63=LM0IY%AIs{r9fiMKloHy}&6aZZvg!u)6B<4$hdsHq3 zC*FS3`?;lnC4jl`?t9Uq)2HPsNj|3*uVOxK6_~;2*5vl&J70Bi@0}7O*BGyV@2;R& z1lr~icE91Fxsecrz|MEdJosmh98npb*Gy+eN6kVLdEJr%27_u`$AHQdaMtHYPHq_R zgJho$881EU#^B7r`yU24+Yf4OE-~0^8y!_`1OrJG2GbO9`>(P@dyOIlC;rODwYkuUa5$NljfaeA(cK<58^8?MnoIHmr3}yvDIV zX9FI~o=u~XKCVOP{XD|Z#YCfZYZUIjOQKX9S0XdLQ$}zx!2*u5%*QzVQ4qVqp5nz# zE(%Qq9ZoQBhT0;sfTx?S0v1qfpeYZWJfYF|T@)+Twt(>bvo8E{QgTxUor!0}Z2CLb zNdnRH5T{#Xo^bSNh`~XB6Dx7w{Ss@QaNw86Q+Gz5d1mW`T5tn!TMN1t3ly3X{y%rl z#PinV3vME`FQn@D+!-^WBfc@us7#%$tp@@)f8NB3*!i407LW=Uxx^SA;wov`wX6)Q z;%+r<$8NCM`risp=i{6o3rri7o=Tg}W zN|x0$_nhOC>nmGqTy!ZAYQlfQ2FR6PrI2R5mn+6~@?lMhC*@`eaph$AzmSjPq>9LL zpe*8lG|UkpX%19DK8_!Ya2@j^tR%)AsCxA}kATq{m=^hnh`e1;c|;D(N`^A-gooQ6 zTMdVWdUkW`_2KyJ!%BfkCj?++S#W(j;N-yL={ex;eh9$Ys-$@|$N6QCmaj0yiv>t! zX8+YqAEhAJ_vn2>yB(%=I^q28D|}xe?fA`q9nfV8h*ugFv78Q=aps(WJek)vdW8GnCQCML)3Z(q` zb|$0dLA@?$y{Y)w>x<&yame1T$Ihl}j1>*hJnh2lI-&h8C|$sRIS&YC^BF{;K2T;v z=ru5H7k-_HrYRyhlosh;Wky;JRP93Uy+D7dOd(b`BR#1M5ryxDdU6pVxwqqQ&7-%W T&1BP200000NkvXXu0mjfNtoI& diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index 8d0752adf770a855f1c0af055b2447ff91a9ecc8..b6d95f1c911dbe843814d18addc38323b934ade9 100644 GIT binary patch literal 15225 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%0DN_UoFdTsg+}G+7$0Gp zeiiLzSHfgvk-4=cZmo9V|2G$XrkA~vci?Vsq561)dSQ})H6c)5!HX6l_?75b@r(3_ zo(&!Vj&-KOy#UpE2gOX#w^rvJBXl!60(z|eFvL=Ec!?e|kXi{6i6CsAQfJ|z=^KVv zFhVe4zGu+%n)fGx4Hde#iFsKXccOm{B62${OtV~QDA6Ewty&^}4W4>|da#f-_65kT z2Can?{{V_b>`WYn%YABmgYUaK^!uI2lf~LPowp~ebD!3&6nB`{(lbFyBxf3Ky|QazOCapG=E(&w`=!!2(nTK=HC0U}uh zL?|y>lD|PT#JfLo8(VV=h)Xd{sg_9^fyspKjx}~ofU->`$2JK`npZx>HwT9zQR{;b z>@xru4j613T3Roe=8ihQ?qG=<)JT`r6{^<~tL=lQ{Yre>71sg6y{+SbaD@@~Uo{3Q z?u)X=grqb*)hi~Q*9KIAkT;7_Y-F<@u&UcjWq%i#LN(tyclMxumuQs?u)HaWNVs*loeFv7d zSGYg_Xh5>I{<`QudGIH`amj6^NKuSd#k+o$UB<}>94Q2U+f&VmS^RsiNftOWOHRH@ zdr>(j&7Ue&Q)SFtfEFh)=pVcDwJ>;sV)~;khpsMn$89_~i_Wwc| zey8wVZ?Fk$aWPDyvrf9flJ<5aHps315;F}17}4;n|F1gf&;Rq)w}*pS zhLCZxQ86F3yLqL`c$}y(g#rQIlZIVXZjRALPU>KtjGy7-n;Hr{gZGomAV(KSc)pK- zG!dcXlCM`&Qm_;dMi5#)G#;q6&b|`aE4+SZJ?uRe-E6MVE>or`vj;wS*e+5>E`Gu? z@yMEmtMJN;)i@ti0M)q26PP9=KpA4}<(LX(5!3{_-gI6isJ>b|Uv~|;{nuo1X=#(x7@(SRb!0*qxQr>YJT#ATD3|y`_ z7Fj#1;jun-DogcYw}YVY@dR-ixQgIwoW!83qm*X#bq0HKy`

O6J5|5f^Y4f0^?oJ^I(ydRlDy=k0p7K2kO8|y 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 literal 1592 zcmZ`)c~BE)6#sU!#7zVVf(q0^fDzA8ZfYqKX|!Y%5Gryh2CINjj&jy0Bt+$?2-;Q= zQ9%YNmdGUnBE+N~v`QO=l28$X$e{#f90@8Zq2DHGb<{t0cISJ?@AuyOeQ&cjd3~iv zHKhUo^jIEDUjPvJi2$4oUt8nL!vN5kW--@p{{GaH9?zJCen#Xw-A`)^bQZbr=+|}a zGH`MZZ3ANmSRQJw8$faQ2<2xP8DFE?P)OwO`on0e<45aNRlkNLafesXIY%dZo8)`_ zBp36ps;Va1N|Y%3V4y-rP*HKCRI$7L(Ed+WsCv9=mnL3VP<^0en6^#5&AZGb(J(N( zWbq$6Pp*|UjvK9cyfU(wWfft77`bkh?ObN;f-Xr5i1x8^Os}!z{g(M>&Ro5WTtGEX zwvVtfwu8n;5_%2094~%(mfPZ-F0_J9p-NNAWDfYj{AR~(hdch*rhjdOrwc_s-iN@w zuJh{T!_aiEn=`j-7Ya?E26sNJ-wTh*!XL8Og)mcSlT##GxE|cl?`TLqT`$xJ5fMM( z#eO1nz;bG0l=M=6=G4Vniog&tavQA*E);EEsiR{WkVNf!tgjlo`FS!Gh+__cAkxCz zb9)izbudxpaL~3F&sN)1|I$)fxx6TCUx>b& z>v@-yQZdt#QQmP?q8JS9oKijKa5y2;r_SWd-OZxix>n_#kuBIR^y7Z9=PqZdLkWVZ z1|G0nCtnHH?2Ee(Pb|>`O(|pLUbz)!)0{0uscF6`V`kYP{aZAHGpue_r%F3j=82F< zi>kL-`C|lo;)%bvG8G?8VB}Fd%jSnixZXDQ>j_+6L_mI-p))(v1)g2F74~ z&8VRQ|CiPWk+d;w2kEr73nGoAw5pw%+`E7TNEIx9_S)BSeJ`gX%W;lf=&fxDoh?lj7%_LO6F;W)7d77 zE`K`VB+}eIaIi`G0?8tmMon5jflJD9-jSV_yJXg=pu{9D%lhl_{3(5>#C{Lo`Ca+& Oj{sQfyqJ~lZ2o`5kxqC3 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) => (
); } + +function CapabilitiesCell({ + capabilities, + onChange, +}: { + capabilities: string[]; + onChange: (next: string[]) => void; +}) { + const { t } = useTranslation(); + const known = new Set(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 ( +
+ {KNOWN_CAPABILITIES.map((cap) => ( + + ))} + { + const newCustoms = e.target.value + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + const kept = capabilities.filter((c) => known.has(c)); + onChange([...kept, ...newCustoms]); + }} + /> +
+ ); +} diff --git a/src/components/ProviderList.tsx b/src/components/ProviderList.tsx index 41fdd5a..a6b18a5 100644 --- a/src/components/ProviderList.tsx +++ b/src/components/ProviderList.tsx @@ -66,9 +66,21 @@ export function ProviderList({ type="button" onClick={onAdd} title={t("addProvider")} - className="w-9 h-9 flex items-center justify-center rounded-full bg-orange-500 hover:bg-orange-600 text-white text-xl shadow-lg focus:ring-2 focus:ring-orange-400 focus:outline-none transition-colors" + className="w-9 h-9 flex items-center justify-center rounded-full bg-orange-500 hover:bg-orange-600 text-white shadow-lg focus:ring-2 focus:ring-orange-400 focus:outline-none transition-colors" > - + + @@ -80,9 +92,21 @@ export function ProviderList({ ) : ( diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 7ca6f89..2e2607f 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -2,7 +2,7 @@ import type { TranslationKey } from "./zh"; export const enTranslations: Record = { // App / global - appTitle: "Pi Switch", + appTitle: "Kimi Switch", loading: "Loading...", loadTimeout: "Loading is taking a while. Try restarting the app if it stays stuck.", loadFailed: "Failed to load:", @@ -58,7 +58,6 @@ export const enTranslations: Record = { fetchingModels: "Fetching...", discoveredModels: "Discovered {count} models", addSelected: "Add selected", - modelRole: "Model role", displayName: "Display name", actualModel: "Actual model", contextSize: "Context size", @@ -88,6 +87,7 @@ export const enTranslations: Record = { switchTo: "Switch to", inUse: "In use", switchReloadHint: "After switching providers, run /reload in Kimi Code to apply the change.", + reloadCopiedHint: "Provider switched. /reload copied to clipboard; paste it in Kimi Code.", unsavedChanges: "Unsaved changes", qwenActivateNotImplemented: "Qwen Code activation will be implemented in a follow-up.", agentSettings: "Global Settings", @@ -123,6 +123,13 @@ export const enTranslations: Record = { importConfigConfirm: "Importing will overwrite unsaved changes. Continue?", importConfigFailed: "Import failed: {message}", capabilities: "Capabilities", + fetchEnableThinking: "Enable thinking for fetched models", + capThinking: "Thinking", + capAlwaysThinking: "Always Thinking", + capImageIn: "Image Input", + capVideoIn: "Video Input", + capToolUse: "Tool Use", + customCapabilities: "Other capabilities (comma-separated)", // Languages language: "Language", diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index f58ba21..6cba560 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -27,7 +27,7 @@ interface I18nContextValue { const I18nContext = createContext(null); -const STORAGE_KEY = "pi-switch-lang"; +const STORAGE_KEY = "kimi-switch-lang"; function getInitialLang(): Language { try { diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index c1594e0..6e1dda3 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -1,6 +1,6 @@ export const zhTranslations = { // App / global - appTitle: "Pi Switch", + appTitle: "Kimi Switch", loading: "加载中...", loadTimeout: "加载时间较长,如果一直卡住,请尝试重新启动应用。", loadFailed: "加载失败:", @@ -56,7 +56,6 @@ export const zhTranslations = { fetchingModels: "获取中...", discoveredModels: "发现 {count} 个模型", addSelected: "添加选中的", - modelRole: "模型角色", displayName: "显示名称", actualModel: "实际请求模型", contextSize: "上下文长度", @@ -86,6 +85,7 @@ export const zhTranslations = { switchTo: "切换使用", inUse: "使用中", switchReloadHint: "切换供应商后,请在 Kimi Code 中执行 /reload 以生效。", + reloadCopiedHint: "已切换供应商,/reload 已复制到剪贴板,请在 Kimi Code 中粘贴执行。", unsavedChanges: "未保存修改", qwenActivateNotImplemented: "Qwen Code 启用功能将在后续实现。", agentSettings: "全局配置", @@ -121,6 +121,13 @@ export const zhTranslations = { importConfigConfirm: "导入会覆盖当前未保存的修改,是否继续?", importConfigFailed: "导入失败:{message}", capabilities: "能力标签", + fetchEnableThinking: "为获取的模型启用思考", + capThinking: "思考", + capAlwaysThinking: "始终思考", + capImageIn: "图像输入", + capVideoIn: "视频输入", + capToolUse: "工具调用", + customCapabilities: "其他能力(逗号分隔)", // Languages language: "语言", diff --git a/src/types/index.ts b/src/types/index.ts index ab80ad6..7f66048 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -34,8 +34,6 @@ export interface Model { model: string; max_context_size: number; display_name: string | null; - /** Optional model role, e.g. Sonnet/Opus/Fable/Haiku for Claude-style mapping. */ - role?: string | null; /** Whether the model declares extended context / thinking support. */ supports_1m?: boolean; /** Agent-specific capability flags (e.g. Kimi Code `capabilities`). */