feat: v0.3.0 - fix provider data loss, oauth corruption & switch UX
Bug fixes: - Fix config.toml wiped to single provider on switch (now writes all providers) - Fix managed:kimi-code oauth key corruption (oauth/kimi-code was rewritten to oauth/managed-kimi-code, breaking the official subscription) - Fix default_model not switching due to React 18 deferred state update (updateConfig now sets configRef synchronously before setConfig) - Strip KimiSwitch-private default_model from config.toml provider output Architecture: - config.toml is now the authoritative source for Kimi Code provider data - SQLite stores only metadata (notes, official URLs, remembered models) - Load reconciles config.toml with SQLite for external /provider changes - Activate is a no-op for KimiCode (save already writes config.toml) Features: - Selected provider moves to top of list on switch - Add models-dev context size lookup - Add tauri-plugin-single-instance Tests: - 3 new regression tests: oauth key round-trip, all-providers write, private default_model strip
This commit is contained in:
1 parent
5806fac056
commit
139af18826
15 files changed
+2398
-97
No files matched your search
Generated
+93
-2
@@ -1952,7 +1952,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kimiswitch"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1965,6 +1965,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-single-instance",
|
||||
"toml 0.8.23",
|
||||
]
|
||||
|
||||
@@ -3773,6 +3774,22 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3214becf9ef5783c0ae99a3bb25adf5353a7a16ebf53e74b909e29205735c6c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.11.3"
|
||||
@@ -4883,6 +4900,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -4931,13 +4957,30 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
@@ -4974,6 +5017,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -4992,6 +5041,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -5010,12 +5065,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -5034,6 +5101,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -5052,6 +5125,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -5070,6 +5149,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -5088,6 +5173,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kimiswitch"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
description = "Kimi Switch - model config manager"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
@@ -11,6 +11,7 @@ tauri-build = { version = "2.0.0", features = [] }
|
||||
[dependencies]
|
||||
tauri = { version = "2.0.0", features = ["tray-icon"] }
|
||||
tauri-plugin-opener = "2.0.0"
|
||||
tauri-plugin-single-instance = "2.0.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
indexmap = { version = "2.2", features = ["serde"] }
|
||||
|
||||
+87
-31
@@ -18,33 +18,77 @@ pub fn debug_log(message: String) {
|
||||
|
||||
#[tauri::command]
|
||||
pub fn load_agent_config_command(agent: Agent) -> Result<Config, String> {
|
||||
// Load from Kimi Switch's own SQLite database first.
|
||||
match db::load_config(&agent) {
|
||||
Ok(config) if !config.providers.is_empty() => Ok(config),
|
||||
_ => {
|
||||
// Fallback to the agent's native config on first use.
|
||||
match agent {
|
||||
Agent::KimiCode => crate::kimi_code_io::load_kimi_code_config_as_config()
|
||||
.map_err(fmt_anyhow),
|
||||
Agent::Pi => {
|
||||
let file = pi_io::load_pi_models().map_err(fmt_anyhow)?;
|
||||
let mut config = pi_io::pi_file_to_config(&file);
|
||||
if config.default_model.is_none() {
|
||||
if let Ok(settings) = pi_io::load_pi_settings() {
|
||||
if let (Some(provider), Some(model_id)) =
|
||||
(settings.default_provider, settings.default_model)
|
||||
{
|
||||
if let Some(alias) = config.models.values().find(|m| {
|
||||
m.provider == provider && m.model == model_id
|
||||
}) {
|
||||
config.default_model = Some(alias.alias.clone());
|
||||
// Load Kimi Switch's own SQLite database (metadata + migration fallback).
|
||||
let db_config = db::load_config(&agent).ok();
|
||||
|
||||
match agent {
|
||||
Agent::KimiCode => {
|
||||
// config.toml is the authoritative source for provider/model data
|
||||
// because the user can add or edit providers at any time via the
|
||||
// CLI's /provider command. SQLite only enriches with Kimi
|
||||
// Switch-private metadata (note, official_url, remembered default
|
||||
// model) and fills gaps when config.toml is incomplete.
|
||||
let mut config = crate::kimi_code_io::load_kimi_code_config_as_config()
|
||||
.map_err(fmt_anyhow)?;
|
||||
|
||||
if let Some(db) = &db_config {
|
||||
// Enrich config.toml providers with SQLite metadata.
|
||||
for (name, p) in config.providers.iter_mut() {
|
||||
if let Some(db_p) = db.providers.get(name) {
|
||||
p.note = db_p.note.clone();
|
||||
p.official_url = db_p.official_url.clone();
|
||||
// Restore the remembered per-provider default model
|
||||
// (Kimi-Switch-private, stored in raw_other).
|
||||
if let Some(dm) = db_p.raw_other.get("default_model") {
|
||||
match &mut p.raw_other {
|
||||
serde_json::Value::Object(obj) => {
|
||||
obj.insert("default_model".to_string(), dm.clone());
|
||||
}
|
||||
_ => {
|
||||
let mut obj = serde_json::Map::new();
|
||||
obj.insert("default_model".to_string(), dm.clone());
|
||||
p.raw_other = serde_json::Value::Object(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
// Migration safety: include providers/models that exist in
|
||||
// SQLite but not in config.toml (e.g. after upgrading from the
|
||||
// old single-provider-write behaviour).
|
||||
for (name, p) in &db.providers {
|
||||
config.providers.entry(name.clone()).or_insert_with(|| p.clone());
|
||||
}
|
||||
for (alias, m) in &db.models {
|
||||
config.models.entry(alias.clone()).or_insert_with(|| m.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
Agent::Pi => {
|
||||
// Pi: SQLite first, fall back to native config on first use.
|
||||
if let Some(config) = db_config {
|
||||
if !config.providers.is_empty() {
|
||||
return Ok(config);
|
||||
}
|
||||
}
|
||||
let file = pi_io::load_pi_models().map_err(fmt_anyhow)?;
|
||||
let mut config = pi_io::pi_file_to_config(&file);
|
||||
if config.default_model.is_none() {
|
||||
if let Ok(settings) = pi_io::load_pi_settings() {
|
||||
if let (Some(provider), Some(model_id)) =
|
||||
(settings.default_provider, settings.default_model)
|
||||
{
|
||||
if let Some(alias) = config.models.values().find(|m| {
|
||||
m.provider == provider && m.model == model_id
|
||||
}) {
|
||||
config.default_model = Some(alias.alias.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,19 +96,30 @@ pub fn load_agent_config_command(agent: Agent) -> Result<Config, String> {
|
||||
#[tauri::command]
|
||||
pub fn save_agent_config_command(agent: Agent, config: Config) -> Result<(), String> {
|
||||
// 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)?;
|
||||
// For Kimi Code, config.toml is the authoritative provider store, so
|
||||
// persist changes there immediately — not only on activation. This
|
||||
// ensures edits (Ctrl+S) survive a restart even without switching.
|
||||
if matches!(agent, Agent::KimiCode) {
|
||||
crate::kimi_code_io::save_config_as_kimi_code(&config).map_err(fmt_anyhow)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn activate_agent_config_command(agent: Agent) -> Result<(), String> {
|
||||
// Load the full config from SQLite and write only the active provider
|
||||
// to the agent's native config file.
|
||||
let config = db::load_config(&agent).map_err(fmt_anyhow)?;
|
||||
let active_config = build_active_config(&config);
|
||||
match agent {
|
||||
Agent::KimiCode => crate::kimi_code_io::save_config_as_kimi_code(&active_config)
|
||||
.map_err(fmt_anyhow),
|
||||
Agent::KimiCode => {
|
||||
// No-op: save_agent_config_command already writes config.toml for
|
||||
// Kimi Code. Avoiding a second write here prevents a redundant disk
|
||||
// write + backup on every switch.
|
||||
Ok(())
|
||||
}
|
||||
Agent::Pi => {
|
||||
// Load the full config from SQLite and write only the active
|
||||
// provider to Pi's native config files.
|
||||
let config = db::load_config(&agent).map_err(fmt_anyhow)?;
|
||||
let active_config = build_active_config(&config);
|
||||
let file = pi_io::config_to_pi_file(&active_config);
|
||||
pi_io::save_pi_models(&file).map_err(fmt_anyhow)?;
|
||||
|
||||
@@ -87,9 +142,10 @@ 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 Kimi Switch's choice
|
||||
// instead of falling back to a managed/native provider.
|
||||
// Used only by Pi: writes only the provider explicitly marked as active
|
||||
// to Pi's native config so Pi follows Kimi Switch's selection instead of
|
||||
// falling back to another provider. Kimi Code does not use this — it
|
||||
// writes all providers and selects via default_model.
|
||||
let providers: IndexMap<String, Provider> = config
|
||||
.providers
|
||||
.iter()
|
||||
|
||||
+152
-17
@@ -174,8 +174,13 @@ pub fn kimi_code_to_config(value: &TomlValue) -> Config {
|
||||
rest.remove("api_key");
|
||||
rest.remove("managed");
|
||||
rest.remove("enabled");
|
||||
rest.remove("oauth");
|
||||
rest.remove("env");
|
||||
// NOTE: `oauth` is intentionally kept in raw_other so that the
|
||||
// exact `storage`/`key` block round-trips verbatim on export.
|
||||
// Previously it was stripped here and regenerated on export
|
||||
// with a key derived from the provider name, which corrupted
|
||||
// the managed:kimi-code credential reference (e.g.
|
||||
// "oauth/kimi-code" became "oauth/managed-kimi-code").
|
||||
toml_value_to_json(&TomlValue::Table(rest))
|
||||
};
|
||||
|
||||
@@ -234,11 +239,10 @@ 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 Kimi Switch's selection.
|
||||
if !provider.active {
|
||||
continue;
|
||||
}
|
||||
// Write ALL providers to Kimi Code config. The active provider is
|
||||
// selected via `default_model`, so keeping the full list matches the
|
||||
// CLI's native multi-provider behavior and prevents data loss when
|
||||
// switching.
|
||||
let mut pt = Table::new();
|
||||
pt.insert("type".to_string(), TomlValue::String(provider.provider_type.as_str().to_string()));
|
||||
if let Some(base_url) = provider.base_url.clone().filter(|s| !s.is_empty()) {
|
||||
@@ -273,6 +277,10 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
|
||||
if let TomlValue::Table(mut extra) = json_to_toml(&provider.raw_other).unwrap_or(TomlValue::Table(Table::new())) {
|
||||
extra.remove("oauth");
|
||||
extra.remove("env");
|
||||
// Strip Kimi-Switch-private field: the remembered per-provider
|
||||
// default model is stored in raw_other.default_model and must NOT
|
||||
// leak into the agent's config.toml.
|
||||
extra.remove("default_model");
|
||||
for (k, v) in extra {
|
||||
pt.insert(k, v);
|
||||
}
|
||||
@@ -281,19 +289,10 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
|
||||
}
|
||||
root.insert("providers".to_string(), TomlValue::Table(providers_table));
|
||||
|
||||
// Collect provider names that survived the filter above.
|
||||
let active_provider_names: std::collections::HashSet<&str> = config
|
||||
.providers
|
||||
.values()
|
||||
.filter(|p| p.active)
|
||||
.map(|p| p.name.as_str())
|
||||
.collect();
|
||||
|
||||
// Write all models — each provider's models are kept so the CLI's
|
||||
// /provider command can list and switch between them.
|
||||
let mut models_table = Table::new();
|
||||
for (alias, model) in &config.models {
|
||||
if !active_provider_names.contains(model.provider.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let mut mt = Table::new();
|
||||
mt.insert("provider".to_string(), TomlValue::String(model.provider.clone()));
|
||||
mt.insert("model".to_string(), TomlValue::String(model.model.clone()));
|
||||
@@ -587,4 +586,140 @@ api_key = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_code_oauth_key_roundtrip() {
|
||||
// Regression: importing then exporting managed:kimi-code must preserve
|
||||
// the exact oauth key "oauth/kimi-code". Previously the oauth block
|
||||
// was dropped on import and regenerated on export as
|
||||
// "oauth/managed-kimi-code", breaking the official subscription.
|
||||
let toml_str = r#"
|
||||
default_model = "kimi-code/k3"
|
||||
|
||||
[providers."managed:kimi-code"]
|
||||
type = "kimi"
|
||||
api_key = ""
|
||||
base_url = "https://api.kimi.com/coding/v1"
|
||||
|
||||
[providers."managed:kimi-code".oauth]
|
||||
storage = "file"
|
||||
key = "oauth/kimi-code"
|
||||
|
||||
[models."kimi-code/k3"]
|
||||
provider = "managed:kimi-code"
|
||||
model = "k3"
|
||||
max_context_size = 1048576
|
||||
"#;
|
||||
let value: TomlValue = toml_str.parse().unwrap();
|
||||
let config = kimi_code_to_config(&value);
|
||||
|
||||
// Export back to TOML.
|
||||
let exported = config_to_kimi_code(&config, None);
|
||||
let root = exported.as_table().unwrap();
|
||||
let providers = root.get("providers").unwrap().as_table().unwrap();
|
||||
let managed = providers.get("managed:kimi-code").unwrap().as_table().unwrap();
|
||||
let oauth = managed.get("oauth").unwrap().as_table().unwrap();
|
||||
assert_eq!(
|
||||
oauth.get("key").and_then(|v| v.as_str()),
|
||||
Some("oauth/kimi-code"),
|
||||
"oauth key must round-trip verbatim, not be regenerated"
|
||||
);
|
||||
assert_eq!(
|
||||
oauth.get("storage").and_then(|v| v.as_str()),
|
||||
Some("file")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_code_export_writes_all_providers() {
|
||||
// Regression: inactive providers must still be written to config.toml
|
||||
// so that switching does not wipe the provider list.
|
||||
let mut providers = IndexMap::new();
|
||||
providers.insert(
|
||||
"active-one".to_string(),
|
||||
Provider {
|
||||
name: "active-one".to_string(),
|
||||
provider_type: ProviderType::Anthropic,
|
||||
base_url: Some("https://a.example.com".to_string()),
|
||||
api_key: Some("sk-a".to_string()),
|
||||
env: IndexMap::new(),
|
||||
note: None,
|
||||
official_url: None,
|
||||
managed: false,
|
||||
enabled: true,
|
||||
active: true,
|
||||
raw_other: Value::Null,
|
||||
},
|
||||
);
|
||||
providers.insert(
|
||||
"inactive-one".to_string(),
|
||||
Provider {
|
||||
name: "inactive-one".to_string(),
|
||||
provider_type: ProviderType::Openai,
|
||||
base_url: Some("https://b.example.com".to_string()),
|
||||
api_key: Some("sk-b".to_string()),
|
||||
env: IndexMap::new(),
|
||||
note: None,
|
||||
official_url: None,
|
||||
managed: false,
|
||||
enabled: true,
|
||||
active: false,
|
||||
raw_other: Value::Null,
|
||||
},
|
||||
);
|
||||
let config = Config {
|
||||
default_model: None,
|
||||
providers,
|
||||
models: IndexMap::new(),
|
||||
raw_other: Value::Null,
|
||||
};
|
||||
|
||||
let exported = config_to_kimi_code(&config, None);
|
||||
let root = exported.as_table().unwrap();
|
||||
let providers_table = root.get("providers").unwrap().as_table().unwrap();
|
||||
assert_eq!(providers_table.len(), 2, "both providers must be written");
|
||||
assert!(providers_table.contains_key("active-one"));
|
||||
assert!(providers_table.contains_key("inactive-one"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_code_export_strips_private_default_model() {
|
||||
// The remembered per-provider default model (raw_other.default_model)
|
||||
// is a Kimi-Switch-private field and must NOT leak into config.toml.
|
||||
let mut providers = IndexMap::new();
|
||||
providers.insert(
|
||||
"p".to_string(),
|
||||
Provider {
|
||||
name: "p".to_string(),
|
||||
provider_type: ProviderType::Anthropic,
|
||||
base_url: None,
|
||||
api_key: Some("sk-x".to_string()),
|
||||
env: IndexMap::new(),
|
||||
note: None,
|
||||
official_url: None,
|
||||
managed: false,
|
||||
enabled: true,
|
||||
active: true,
|
||||
raw_other: serde_json::json!({"default_model": "some-alias"}),
|
||||
},
|
||||
);
|
||||
let config = Config {
|
||||
default_model: None,
|
||||
providers,
|
||||
models: IndexMap::new(),
|
||||
raw_other: Value::Null,
|
||||
};
|
||||
|
||||
let exported = config_to_kimi_code(&config, None);
|
||||
let root = exported.as_table().unwrap();
|
||||
let provider = root
|
||||
.get("providers").unwrap()
|
||||
.as_table().unwrap()
|
||||
.get("p").unwrap()
|
||||
.as_table().unwrap();
|
||||
assert!(
|
||||
!provider.contains_key("default_model"),
|
||||
"private default_model must not leak into config.toml"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,16 @@ use tauri::Manager;
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
// Single instance: must be registered before other plugins. When a
|
||||
// second process is launched, this callback runs in the existing
|
||||
// instance and simply brings its window back (including from tray).
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
println!("[Tauri] Setup started");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"productName": "Kimi Switch",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"identifier": "com.kimiswitch.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
Reference in new issue
Block a user