Compare commits

..
8 Commits
Author SHA1 Message Date
KimiSwitch Dev 803c346854 fix(dashboard): useDashboard 加请求世代校验 — 快速连续切换时间范围时并发 get_summary 响应可能乱序返回导致标签与数据错位;参照 useUsageQuery 的 genRef 模式丢弃过期响应(data/loadStats/error/loading 全部受保护),卸载时使世代失效 2026-10-08 00:28:39 +08:00
KimiSwitch Dev f9f9af0fc9 perf(dashboard): 扫描输入缓存 + 归档合并零浪费 + 锁加固 — resolve_scan_inputs 结果按 config.toml/备份文件 (mtime,len) stat 指纹缓存(命中跳过 15 个配置文件 TOML 解析与 SQLite 读写,set_setting_pub 仅在值变化时写);归档合并先合成后克隆,合成结果为空时原 Arc 直接返回(跳过 6.3 万条 clone+sort,已排序输入的稳定排序为恒等变换);SCAN_CACHE/SCAN_INPUTS_CACHE 改 into_inner 防 Mutex 中毒;新增 4 个测试(Arc::ptr_eq 正反例、配置失效链、跨 home 隔离)并修复测试间 KIMI_SWITCH_DB_PATH 全局 env 并行污染;聚合口径零变化 2026-10-08 00:28:22 +08:00
KimiSwitch Dev 9f51ea5e84 perf(dashboard): 加载统计去掉对整个返回值的 JSON.stringify(数 MB 大对象在 webview 主线程序列化造成卡顿),loadStats 仅保留耗时 ms,中英文案同步 2026-10-07 23:35:30 +08:00
KimiSwitch Dev d190a55bf2 perf(dashboard): 用量统计性能优化 — get_summary/get_day_detail 改异步执行不再阻塞 UI;range_totals 单遍统计(每次调用 7 次全量聚合 → 2 次);扫描缓存改 Arc 共享消除整表深拷贝;wire.jsonl 按 (mtime,len) 逐文件增量解析替换 8s TTL 整表缓存(920MB/6.3万条实测:冷扫描 ~17.7s → ~1.0s,任意间隔切换范围 ~1.5s 且 UI 全程可交互);新增单遍等价性与增量行为测试(含混合增删改 vs 强制全量重扫的全字段指纹比对),聚合口径零变化 2026-10-07 23:35:17 +08:00
KimiSwitch Dev ac82135dee feat(settings): 适配 kimi-code 新增 auto_session_title / repeat_breaker 顶层开关;最大输出未匹配回填 131072;watch 默认值修正 — bump to v0.8.1
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s
包含未发布的 ef6749b / 24cb04b / 977f433 三个提交,官网同步更新至 v0.8.1
2026-10-04 00:59:59 +08:00
KimiSwitch Dev ef6749b208 feat(models): 最大输出未匹配 models.dev 时回填默认值 131072(打开编辑页一次性回填 + 获取模型列表预填同口径) 2026-10-04 00:51:50 +08:00
KimiSwitch Dev 24cb04b98f style(i18n): 「最大输出 Token」列名精简为「最大输出」 2026-10-04 00:46:52 +08:00
KimiSwitch Dev 977f433bb2 fix(settings): 适配 kimi-code 近期变更 — [watch] 默认值修正为开启(上游 #4015 翻回默认开);新增顶层开关 auto_session_title(#3962)与 repeat_breaker(#3995,默认开、false 显式写入/true 删键);最大输出 Token 文案修正为「留空则不发送输出上限」(上游 #4091 未配置即省略 max_tokens) 2026-10-04 00:26:05 +08:00
22 changed files with 1237 additions and 181 deletions

No files matched your search

+7
View File
@@ -6,6 +6,13 @@
--- ---
## v0.8.1 (2026-10-04)
- **全局配置新增两个顶层开关**:自动生成会话标题(`auto_session_title`)与重复调用拦截(`repeat_breaker`),默认开启,关闭显式写 `false`、开启删键跟随上游默认(适配 kimi-code #3962 / #3995)
- **最大输出未匹配 models.dev 时回填默认值 131072**(128K 兜底,编辑页回填与获取模型列表预填同口径)
- **修复 `[watch] enabled` 默认值显示**:上游 #4015 已翻回默认开启,设置页同步修正
- **「最大输出 Token」列名精简为「最大输出」**,描述对齐上游 #4091(留空即不发送输出上限)
## v0.8.0 (2026-10-03) ## v0.8.0 (2026-10-03)
- **模型映射新增「最大输出 Token」列**:逐模型设置 `max_output_size`(写入 config.toml,留空用上游默认),models.dev 有 output 上限时显示可点击参考值一键填入;获取模型列表时自动预填 - **模型映射新增「最大输出 Token」列**:逐模型设置 `max_output_size`(写入 config.toml,留空用上游默认),models.dev 有 output 上限时显示可点击参考值一键填入;获取模型列表时自动预填
@@ -0,0 +1,17 @@
# KimiSwitch v0.8.1
## 新功能
- **全局配置新增两个顶层开关**(适配 kimi-code 近期版本):
- **自动生成会话标题**(`auto_session_title`,上游 #3962):允许客户端自动生成会话标题,默认开启;关闭后显式写入 `false`,开启即删键跟随上游默认
- **重复调用拦截**(`repeat_breaker`,上游 #3995):连续重复相同工具调用时提醒并强制停止,默认开启;可被环境变量 `KIMI_CODE_REPEAT_BREAKER` 覆盖
- **最大输出未匹配时回填默认值 131072**(Kimi Code):打开供应商编辑页自动回填时,models.dev 未收录的模型按 128K(131072)兜底;「获取模型列表」新增模型同口径预填
## 修复
- **`[watch] enabled` 默认值修正为开启**:kimi-code #4015 已把文件监听默认值翻回开启(2.0.2 曾短暂默认关闭),设置页的开关显示默认值同步修正
- **「最大输出 Token」列名精简为「最大输出」**;列描述修正为「留空则不发送输出上限」(对齐上游 #4091:未显式配置时不发送 `max_tokens`,避免严格服务栈如 bare vLLM 报 400)
## 备注
- 前端 130 测试 / Rust 162 测试全量通过
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "kimiswitch", "name": "kimiswitch",
"private": true, "private": true,
"version": "0.8.0", "version": "0.8.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+16 -15
View File
@@ -23338,9 +23338,9 @@
"tool_call": true, "tool_call": true,
"structured_output": true, "structured_output": true,
"cost": { "cost": {
"input": 0.13, "input": 0.0825,
"output": 0.53, "output": 0.33,
"cache_read": 0.033 "cache_read": 0.020625
} }
}, },
"kilo/tencent/hy-mt2-7b": { "kilo/tencent/hy-mt2-7b": {
@@ -23361,9 +23361,9 @@
"tool_call": true, "tool_call": true,
"structured_output": true, "structured_output": true,
"cost": { "cost": {
"input": 0.834, "input": 0.7506,
"output": 2.501, "output": 2.2509,
"cache_read": 0.042 "cache_read": 0.0378
} }
}, },
"kilo/anthracite-org/magnum-v4-72b": { "kilo/anthracite-org/magnum-v4-72b": {
@@ -25260,9 +25260,10 @@
"image": true, "image": true,
"video": true, "video": true,
"cost": { "cost": {
"input": 0.99, "input": 3,
"output": 13, "output": 15,
"cache_read": 0.8 "cache_read": 0.3,
"cache_write": 3.75
} }
}, },
"kilo/moonshotai/kimi-k2.6": { "kilo/moonshotai/kimi-k2.6": {
@@ -35356,9 +35357,9 @@
"tool_call": true, "tool_call": true,
"structured_output": true, "structured_output": true,
"cost": { "cost": {
"input": 0.132, "input": 0.0825,
"output": 0.528, "output": 0.33,
"cache_read": 0.033 "cache_read": 0.020625
} }
}, },
"openrouter/tencent/hy-mt2-7b": { "openrouter/tencent/hy-mt2-7b": {
@@ -35379,9 +35380,9 @@
"tool_call": true, "tool_call": true,
"structured_output": true, "structured_output": true,
"cost": { "cost": {
"input": 0.834, "input": 0.7506,
"output": 2.501, "output": 2.2509,
"cache_read": 0.042 "cache_read": 0.0378
} }
}, },
"openrouter/anthracite-org/magnum-v4-72b": { "openrouter/anthracite-org/magnum-v4-72b": {
+1 -1
View File
@@ -1978,7 +1978,7 @@ dependencies = [
[[package]] [[package]]
name = "kimiswitch" name = "kimiswitch"
version = "0.8.0" version = "0.8.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "kimiswitch" name = "kimiswitch"
version = "0.8.0" version = "0.8.1"
description = "Kimi Switch - model config manager" description = "Kimi Switch - model config manager"
authors = ["codingplan.site"] authors = ["codingplan.site"]
edition = "2021" edition = "2021"
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"productName": "Kimi Switch", "productName": "Kimi Switch",
"version": "0.8.0", "version": "0.8.1",
"identifier": "com.kimiswitch.app", "identifier": "com.kimiswitch.app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
+24 -6
View File
@@ -236,19 +236,37 @@ export function AgentSettingsPanel({ rawOther, onChange, models, defaultModel }:
</Card> </Card>
<Card title={t("watchSettings")}> <Card title={t("watchSettings")}>
{/* Top-level `[watch] enabled` (kimi-code 2.0.1+; off by default since {/* Top-level `[watch] enabled` (kimi-code 2.0.1+; 2.0.2 flipped the
2.0.2). KIMI_CODE_WATCH outranks this config at runtime — the env default off, #4015 flipped it back on). KIMI_CODE_WATCH outranks
var is probed by get_experimental_env_status as a non-flag entry this config at runtime — the env var is probed by
but deliberately locks nothing here, the toggle just documents the get_experimental_env_status as a non-flag entry but deliberately
config value that applies when the env var is unset. */} locks nothing here, the toggle just documents the config value
that applies when the env var is unset. */}
<Checkbox <Checkbox
label={t("watchEnabled")} label={t("watchEnabled")}
checked={settings.watch?.enabled ?? false} checked={settings.watch?.enabled ?? true}
onChange={(checked) => updateWatch({ enabled: checked })} onChange={(checked) => updateWatch({ enabled: checked })}
/> />
<p className="text-xs text-content-muted">{t("watchEnabledDesc")}</p> <p className="text-xs text-content-muted">{t("watchEnabledDesc")}</p>
</Card> </Card>
<Card title={t("behaviorSettings")}>
{/* Top-level booleans, both default true upstream: false is written
explicitly, true removes the key (see setAgentSettings). */}
<Checkbox
label={t("autoSessionTitle")}
checked={settings.auto_session_title ?? true}
onChange={(checked) => update({ auto_session_title: checked })}
/>
<p className="text-xs text-content-muted">{t("autoSessionTitleDesc")}</p>
<Checkbox
label={t("repeatBreaker")}
checked={settings.repeat_breaker ?? true}
onChange={(checked) => update({ repeat_breaker: checked })}
/>
<p className="text-xs text-content-muted">{t("repeatBreakerDesc")}</p>
</Card>
<Card title={t("permissionRules")}> <Card title={t("permissionRules")}>
{/* kimi-code `[permission] dangerous_command_guard`. The env {/* kimi-code `[permission] dangerous_command_guard`. The env
var KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false") var KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false")
+22 -9
View File
@@ -39,6 +39,9 @@ const CAPABILITY_LABELS: Record<
tool_use: "capToolUse", tool_use: "capToolUse",
}; };
/** Fallback max-output cap (128K) for models models.dev doesn't know. */
const FALLBACK_MAX_OUTPUT = 131_072;
const PROVIDER_TYPES: ProviderType[] = [ const PROVIDER_TYPES: ProviderType[] = [
"openai", "openai",
"openai_responses", "openai_responses",
@@ -577,8 +580,9 @@ function ModelMapping({
// the max-output "参考" hints appear even when this panel mounted first. // the max-output "参考" hints appear even when this panel mounted first.
const [, forceModelsDevReady] = useReducer((x: number) => x + 1, 0); const [, forceModelsDevReady] = useReducer((x: number) => x + 1, 0);
// One-shot backfill: models with no max_output_size yet get the models.dev // One-shot backfill: models with no max_output_size yet get the models.dev
// `output` cap filled in automatically. onModelChange only mutates the // `output` cap, or the 128K fallback when nothing matches. onModelChange
// in-memory config — the user still reviews and presses 保存配置. // only mutates the in-memory config — the user still reviews and presses
// 保存配置.
const backfillDone = useRef(false); const backfillDone = useRef(false);
const modelsRef = useRef(models); const modelsRef = useRef(models);
modelsRef.current = models; modelsRef.current = models;
@@ -591,8 +595,12 @@ function ModelMapping({
backfillDone.current = true; backfillDone.current = true;
for (const m of modelsRef.current) { for (const m of modelsRef.current) {
if (readMaxOutputSize(m.raw_other) !== undefined) continue; if (readMaxOutputSize(m.raw_other) !== undefined) continue;
const output = getModelRef(m.model)?.output; onModelChange(
if (output !== undefined) onModelChange(withMaxOutputSize(m, output)); withMaxOutputSize(
m,
getModelRef(m.model)?.output ?? FALLBACK_MAX_OUTPUT
)
);
} }
}); });
return () => { return () => {
@@ -669,11 +677,16 @@ function ModelMapping({
: fetchThinking : fetchThinking
? ["thinking"] ? ["thinking"]
: [], : [],
// Seed the max_output_size override only when models.dev knows the // Seed the max_output_size override with the models.dev cap, falling
// cap — an absent key means the upstream default applies. kimi_code // back to 128K when nothing matches. kimi_code only: Pi's equivalent
// only: Pi's equivalent is `maxTokens` (not written by this UI). // is `maxTokens` (not written by this UI).
...(agent === "kimi_code" && ref?.output ...(agent === "kimi_code"
? { raw_other: setMaxOutputSize(undefined, ref.output) } ? {
raw_other: setMaxOutputSize(
undefined,
ref?.output ?? FALLBACK_MAX_OUTPUT
),
}
: {}), : {}),
}); });
} }
+1 -1
View File
@@ -179,7 +179,7 @@ export function DashboardPage() {
</div> </div>
{loadStats && !loading && ( {loadStats && !loading && (
<span className="hidden text-[10px] text-content-muted sm:inline"> <span className="hidden text-[10px] text-content-muted sm:inline">
{t("loadStats", { ms: loadStats.ms, kb: loadStats.kb })} {t("loadStats", { ms: loadStats.ms })}
</span> </span>
)} )}
</div> </div>
+16 -7
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type { SummaryResult } from "../types/dashboard"; import type { SummaryResult } from "../types/dashboard";
@@ -23,9 +23,12 @@ export function useDashboard() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
/** Round-trip timing for the last get_summary call (user-perceived lag). */ /** Round-trip timing for the last get_summary call (user-perceived lag). */
const [loadStats, setLoadStats] = useState<{ ms: number; kb: number } | null>(null); const [loadStats, setLoadStats] = useState<{ ms: number } | null>(null);
// Generation counter: stale responses (superseded range / unmounted) are dropped.
const genRef = useRef(0);
const refresh = useCallback(async (force = false) => { const refresh = useCallback(async (force = false) => {
const gen = ++genRef.current;
setLoading(true); setLoading(true);
setError(null); setError(null);
const start = performance.now(); const start = performance.now();
@@ -34,16 +37,15 @@ export function useDashboard() {
range, range,
refresh: force, refresh: force,
}); });
if (genRef.current !== gen) return;
setData(result); setData(result);
setLoadStats({ setLoadStats({ ms: Math.round(performance.now() - start) });
ms: Math.round(performance.now() - start),
kb: Math.round(JSON.stringify(result).length / 1024),
});
} catch (err) { } catch (err) {
if (genRef.current !== gen) return;
const msg = err instanceof Error ? err.message : String(err); const msg = err instanceof Error ? err.message : String(err);
setError(msg); setError(msg);
} finally { } finally {
setLoading(false); if (genRef.current === gen) setLoading(false);
} }
}, [range]); }, [range]);
@@ -51,6 +53,13 @@ export function useDashboard() {
refresh(); refresh();
}, [refresh]); }, [refresh]);
// Ignore late responses after unmount.
useEffect(() => {
return () => {
genRef.current += 1;
};
}, []);
const changeRange = useCallback((next: DashboardRange) => { const changeRange = useCallback((next: DashboardRange) => {
setRange(next); setRange(next);
try { try {
+11 -4
View File
@@ -112,7 +112,7 @@ export const enTranslations: Record<TranslationKey, string> = {
envPairs: "Env pairs", envPairs: "Env pairs",
addEnv: "+ Add", addEnv: "+ Add",
modelMappingDesc: "Display name only affects the /model menu.", modelMappingDesc: "Display name only affects the /model menu.",
maxOutputSizeDesc: "Max output tokens writes max_output_size — leave blank to use the upstream default.", maxOutputSizeDesc: "Max output writes max_output_size — leave blank to send no output cap upstream.",
oneClickSetup: "One-click setup", oneClickSetup: "One-click setup",
fetchModels: "Fetch models", fetchModels: "Fetch models",
fetchingModels: "Fetching...", fetchingModels: "Fetching...",
@@ -122,7 +122,7 @@ export const enTranslations: Record<TranslationKey, string> = {
displayName: "Display name", displayName: "Display name",
actualModel: "Actual model", actualModel: "Actual model",
contextSize: "Context size", contextSize: "Context size",
maxOutputSize: "Max output tokens", maxOutputSize: "Max output",
maxOutputRef: "Ref {value}", maxOutputRef: "Ref {value}",
default: "Default", default: "Default",
operation: "Operation", operation: "Operation",
@@ -188,7 +188,14 @@ export const enTranslations: Record<TranslationKey, string> = {
watchSettings: "File Watching", watchSettings: "File Watching",
watchEnabled: "Enable file watching", watchEnabled: "Enable file watching",
watchEnabledDesc: watchEnabledDesc:
"Watch config files and the workspace for changes (configurable since kimi-code 2.0.1, off by default since 2.0.2). The KIMI_CODE_WATCH environment variable overrides this setting.", "Watch config files and the workspace for changes (configurable since kimi-code 2.0.1, on by default). The KIMI_CODE_WATCH environment variable overrides this setting.",
behaviorSettings: "Behavior",
autoSessionTitle: "Auto session title",
autoSessionTitleDesc:
"Let clients auto-generate session titles (on by default; turning it off writes false).",
repeatBreaker: "Repeat breaker",
repeatBreakerDesc:
"Remind and force-stop on consecutively repeated identical tool calls (on by default). The KIMI_CODE_REPEAT_BREAKER environment variable overrides this setting.",
permissionRules: "Permission Rules", permissionRules: "Permission Rules",
permissionDecision: "Decision", permissionDecision: "Decision",
permissionPattern: "Pattern", permissionPattern: "Pattern",
@@ -250,7 +257,7 @@ export const enTranslations: Record<TranslationKey, string> = {
scanning: "Scanning…", scanning: "Scanning…",
refresh: "Refresh", refresh: "Refresh",
refreshing: "Refreshing…", refreshing: "Refreshing…",
loadStats: "Loaded in {ms} ms · {kb} KB payload", loadStats: "Loaded in {ms} ms",
refreshed: "Refreshed", refreshed: "Refreshed",
cancel: "Cancel", cancel: "Cancel",
confirmDeleteTitle: "Delete session forever?", confirmDeleteTitle: "Delete session forever?",
+11 -4
View File
@@ -109,7 +109,7 @@ export const zhTranslations = {
envPairs: "Env 键值对", envPairs: "Env 键值对",
addEnv: "+ 添加", addEnv: "+ 添加",
modelMappingDesc: "显示名称只影响 /model 菜单。", modelMappingDesc: "显示名称只影响 /model 菜单。",
maxOutputSizeDesc: "「最大输出 Token」写入 max_output_size,留空则用上游默认值。", maxOutputSizeDesc: "「最大输出」写入 max_output_size,留空则不发送输出上限。",
oneClickSetup: "一键设置", oneClickSetup: "一键设置",
fetchModels: "获取模型列表", fetchModels: "获取模型列表",
fetchingModels: "获取中...", fetchingModels: "获取中...",
@@ -119,7 +119,7 @@ export const zhTranslations = {
displayName: "显示名称", displayName: "显示名称",
actualModel: "实际请求模型", actualModel: "实际请求模型",
contextSize: "上下文长度", contextSize: "上下文长度",
maxOutputSize: "最大输出 Token", maxOutputSize: "最大输出",
maxOutputRef: "参考 {value}", maxOutputRef: "参考 {value}",
default: "默认", default: "默认",
operation: "操作", operation: "操作",
@@ -181,7 +181,14 @@ export const zhTranslations = {
watchSettings: "文件监听", watchSettings: "文件监听",
watchEnabled: "启用文件监听", watchEnabled: "启用文件监听",
watchEnabledDesc: watchEnabledDesc:
"监听配置文件与工作目录变化(kimi-code 2.0.1 起可配置,2.0.2 起默认关闭)。环境变量 KIMI_CODE_WATCH 会覆盖本配置。", "监听配置文件与工作目录变化(kimi-code 2.0.1 起可配置,默认开启)。环境变量 KIMI_CODE_WATCH 会覆盖本配置。",
behaviorSettings: "行为开关",
autoSessionTitle: "自动生成会话标题",
autoSessionTitleDesc:
"允许客户端自动生成会话标题(默认开启;关闭后写入 false)。",
repeatBreaker: "重复调用拦截",
repeatBreakerDesc:
"连续重复相同工具调用时提醒并强制停止(默认开启)。环境变量 KIMI_CODE_REPEAT_BREAKER 会覆盖本配置。",
permissionRules: "权限规则", permissionRules: "权限规则",
permissionDecision: "处置", permissionDecision: "处置",
permissionPattern: "模式", permissionPattern: "模式",
@@ -242,7 +249,7 @@ export const zhTranslations = {
scanning: "扫描中…", scanning: "扫描中…",
refresh: "刷新", refresh: "刷新",
refreshing: "刷新中…", refreshing: "刷新中…",
loadStats: "加载 {ms} ms · 数据 {kb} KB", loadStats: "加载 {ms} ms",
refreshed: "已刷新", refreshed: "已刷新",
cancel: "取消", cancel: "取消",
confirmDeleteTitle: "永久删除会话?", confirmDeleteTitle: "永久删除会话?",
+48 -5
View File
@@ -201,8 +201,8 @@ describe("loop_control.compaction_max_attempts", () => {
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// [watch] enabled (kimi-code 2.0.1+) — top-level section, off by default since // [watch] enabled (kimi-code 2.0.1+) — top-level section; 2.0.2 flipped the
// 2.0.2; mirrors the [background] handling // default off, #4015 flipped it back on. Mirrors the [background] handling.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function watchOf(raw: unknown): Record<string, unknown> { function watchOf(raw: unknown): Record<string, unknown> {
@@ -213,9 +213,9 @@ function watchOf(raw: unknown): Record<string, unknown> {
} }
describe("[watch] enabled", () => { describe("[watch] enabled", () => {
it("reads as false when the section or key is absent (2.0.2 default)", () => { it("reads as true when the section or key is absent (#4015 default)", () => {
expect(getAgentSettings({}).watch?.enabled).toBe(false); expect(getAgentSettings({}).watch?.enabled).toBe(true);
expect(getAgentSettings({ watch: {} }).watch?.enabled).toBe(false); expect(getAgentSettings({ watch: {} }).watch?.enabled).toBe(true);
}); });
it("reads an explicit value", () => { it("reads an explicit value", () => {
@@ -248,6 +248,49 @@ describe("[watch] enabled", () => {
}); });
}); });
// ---------------------------------------------------------------------------
// Top-level booleans (auto_session_title #3962 / repeat_breaker #3995):
// upstream default true — false is written explicitly, true removes the key.
// ---------------------------------------------------------------------------
describe("top-level boolean switches", () => {
it("reads as undefined when the key is absent (default true)", () => {
const s = getAgentSettings({});
expect(s.auto_session_title).toBeUndefined();
expect(s.repeat_breaker).toBeUndefined();
});
it("reads explicit false values", () => {
const s = getAgentSettings({
auto_session_title: false,
repeat_breaker: false,
});
expect(s.auto_session_title).toBe(false);
expect(s.repeat_breaker).toBe(false);
});
it("writes false explicitly and removes the key on true", () => {
const off = setAgentSettings({}, { repeat_breaker: false }) as Record<
string,
unknown
>;
expect(off.repeat_breaker).toBe(false);
const on = setAgentSettings(
{ repeat_breaker: false },
{ repeat_breaker: true }
) as Record<string, unknown>;
expect("repeat_breaker" in on).toBe(false);
});
it("keeps an explicit false on saves that do not touch it", () => {
const next = setAgentSettings(
{ auto_session_title: false },
{ thinking: { effort: "high" } }
) as Record<string, unknown>;
expect(next.auto_session_title).toBe(false);
});
});
describe("permission.dangerous_command_guard", () => { describe("permission.dangerous_command_guard", () => {
it("reads as undefined when the key is absent (default on)", () => { it("reads as undefined when the key is absent (default on)", () => {
+27 -3
View File
@@ -13,10 +13,11 @@ const DEFAULT_SETTINGS: AgentSettings = {
background: { background: {
keep_alive_on_exit: false, keep_alive_on_exit: false,
}, },
// `[watch] enabled` — kimi-code 2.0.1 added the key, 2.0.2 flipped the // `[watch] enabled` — kimi-code 2.0.1 added the key; 2.0.2 flipped the
// default to off, so an absent key reads as "off". // default to off, then #4015 flipped it back on, so an absent key reads
// as "on".
watch: { watch: {
enabled: false, enabled: true,
}, },
permission: { rules: [] }, permission: { rules: [] },
hooks: [], hooks: [],
@@ -37,6 +38,7 @@ function getSection<T>(rawOther: unknown, key: string): T | undefined {
} }
export function getAgentSettings(rawOther: unknown): AgentSettings { export function getAgentSettings(rawOther: unknown): AgentSettings {
const root = asRecord(rawOther);
const sectionLoop = getSection<AgentSettings["loop_control"]>( const sectionLoop = getSection<AgentSettings["loop_control"]>(
rawOther, rawOther,
"loop_control" "loop_control"
@@ -92,6 +94,16 @@ export function getAgentSettings(rawOther: unknown): AgentSettings {
: {}), : {}),
}, },
hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [], hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [],
// Top-level booleans (upstream default true): keep only explicit values,
// an absent key means "on" — the UI shows `?? true`.
auto_session_title:
typeof root.auto_session_title === "boolean"
? root.auto_session_title
: undefined,
repeat_breaker:
typeof root.repeat_breaker === "boolean"
? root.repeat_breaker
: undefined,
}; };
} }
@@ -166,5 +178,17 @@ export function setAgentSettings(
delete root.hooks; delete root.hooks;
} }
// Top-level booleans with an upstream default of true: an explicit false
// is written; true/undefined removes the key so the config tracks the
// upstream default instead of pinning it.
for (const key of ["auto_session_title", "repeat_breaker"] as const) {
const value = patch[key] ?? current[key];
if (value === false) {
root[key] = false;
} else {
delete root[key];
}
}
return root; return root;
} }
+16 -15
View File
@@ -26175,9 +26175,9 @@
"id": "tencent/hy3", "id": "tencent/hy3",
"name": "Hy3", "name": "Hy3",
"cost": { "cost": {
"input": 0.13, "input": 0.0825,
"output": 0.53, "output": 0.33,
"cache_read": 0.033 "cache_read": 0.020625
}, },
"context": 262144, "context": 262144,
"input_limit": 192000, "input_limit": 192000,
@@ -26201,9 +26201,9 @@
"id": "tencent/hy4-preview", "id": "tencent/hy4-preview",
"name": "Hy4 preview", "name": "Hy4 preview",
"cost": { "cost": {
"input": 0.834, "input": 0.7506,
"output": 2.501, "output": 2.2509,
"cache_read": 0.042 "cache_read": 0.0378
}, },
"context": 1048576, "context": 1048576,
"output_limit": 64000, "output_limit": 64000,
@@ -28245,9 +28245,10 @@
"id": "moonshotai/kimi-k3", "id": "moonshotai/kimi-k3",
"name": "Kimi K3", "name": "Kimi K3",
"cost": { "cost": {
"input": 0.99, "input": 3,
"output": 13, "output": 15,
"cache_read": 0.8 "cache_read": 0.3,
"cache_write": 3.75
}, },
"context": 1048576, "context": 1048576,
"output_limit": 943718, "output_limit": 943718,
@@ -39344,9 +39345,9 @@
"id": "tencent/hy3", "id": "tencent/hy3",
"name": "Hy3", "name": "Hy3",
"cost": { "cost": {
"input": 0.132, "input": 0.0825,
"output": 0.528, "output": 0.33,
"cache_read": 0.033 "cache_read": 0.020625
}, },
"context": 262144, "context": 262144,
"input_limit": 192000, "input_limit": 192000,
@@ -39370,9 +39371,9 @@
"id": "tencent/hy4-preview", "id": "tencent/hy4-preview",
"name": "Hy4 preview", "name": "Hy4 preview",
"cost": { "cost": {
"input": 0.834, "input": 0.7506,
"output": 2.501, "output": 2.2509,
"cache_read": 0.042 "cache_read": 0.0378
}, },
"context": 1048576, "context": 1048576,
"output_limit": 64000, "output_limit": 64000,
+16 -15
View File
@@ -23338,9 +23338,9 @@
"tool_call": true, "tool_call": true,
"structured_output": true, "structured_output": true,
"cost": { "cost": {
"input": 0.13, "input": 0.0825,
"output": 0.53, "output": 0.33,
"cache_read": 0.033 "cache_read": 0.020625
} }
}, },
"kilo/tencent/hy-mt2-7b": { "kilo/tencent/hy-mt2-7b": {
@@ -23361,9 +23361,9 @@
"tool_call": true, "tool_call": true,
"structured_output": true, "structured_output": true,
"cost": { "cost": {
"input": 0.834, "input": 0.7506,
"output": 2.501, "output": 2.2509,
"cache_read": 0.042 "cache_read": 0.0378
} }
}, },
"kilo/anthracite-org/magnum-v4-72b": { "kilo/anthracite-org/magnum-v4-72b": {
@@ -25260,9 +25260,10 @@
"image": true, "image": true,
"video": true, "video": true,
"cost": { "cost": {
"input": 0.99, "input": 3,
"output": 13, "output": 15,
"cache_read": 0.8 "cache_read": 0.3,
"cache_write": 3.75
} }
}, },
"kilo/moonshotai/kimi-k2.6": { "kilo/moonshotai/kimi-k2.6": {
@@ -35356,9 +35357,9 @@
"tool_call": true, "tool_call": true,
"structured_output": true, "structured_output": true,
"cost": { "cost": {
"input": 0.132, "input": 0.0825,
"output": 0.528, "output": 0.33,
"cache_read": 0.033 "cache_read": 0.020625
} }
}, },
"openrouter/tencent/hy-mt2-7b": { "openrouter/tencent/hy-mt2-7b": {
@@ -35379,9 +35380,9 @@
"tool_call": true, "tool_call": true,
"structured_output": true, "structured_output": true,
"cost": { "cost": {
"input": 0.834, "input": 0.7506,
"output": 2.501, "output": 2.2509,
"cache_read": 0.042 "cache_read": 0.0378
} }
}, },
"openrouter/anthracite-org/magnum-v4-72b": { "openrouter/anthracite-org/magnum-v4-72b": {
+7
View File
@@ -172,6 +172,13 @@ export interface AgentSettings {
background?: BackgroundConfig; background?: BackgroundConfig;
/** Top-level `[watch]` section (kimi-code 2.0.1+). */ /** Top-level `[watch]` section (kimi-code 2.0.1+). */
watch?: WatchConfig; watch?: WatchConfig;
/** Top-level config.toml key (kimi-code #3962): let clients auto-generate
* session titles. Default true; only an explicit false disables. */
auto_session_title?: boolean;
/** Top-level config.toml key (kimi-code #3995): repeat-breaker for
* consecutively repeated identical tool calls. Default true; env
* KIMI_CODE_REPEAT_BREAKER outranks this config. */
repeat_breaker?: boolean;
permission?: { permission?: {
rules?: PermissionRule[]; rules?: PermissionRule[];
/** kimi-code `[permission]` key. Default true when the key is /** kimi-code `[permission]` key. Default true when the key is
+1 -1
View File
@@ -50,7 +50,7 @@
"url": "https://billowliu2.github.io/KimiSwitch/", "url": "https://billowliu2.github.io/KimiSwitch/",
"applicationCategory": "DeveloperApplication", "applicationCategory": "DeveloperApplication",
"operatingSystem": "Windows 10, Windows 11, macOS, Linux", "operatingSystem": "Windows 10, Windows 11, macOS, Linux",
"softwareVersion": "0.8.0", "softwareVersion": "0.8.1",
"dateModified": "2026-09-03", "dateModified": "2026-09-03",
"description": "桌面端 LLM 供应商配置管理器:22 个预设选好即写入 config.toml,用量与账单直读各厂商官方接口。Tauri + Rust,开源 MIT。", "description": "桌面端 LLM 供应商配置管理器:22 个预设选好即写入 config.toml,用量与账单直读各厂商官方接口。Tauri + Rust,开源 MIT。",
"downloadUrl": "https://github.com/billowliu2/KimiSwitch/releases", "downloadUrl": "https://github.com/billowliu2/KimiSwitch/releases",
+18
View File
@@ -140,6 +140,15 @@ const zh = {
syncedNote: "数据已同步 GitHub Releases", syncedNote: "数据已同步 GitHub Releases",
fallbackNote: "内置版本记录", fallbackNote: "内置版本记录",
entries: [ entries: [
{
version: "v0.8.1",
date: "2026-10-04",
items: [
"全局配置新增两个顶层开关:自动生成会话标题(auto_session_title)与重复调用拦截(repeat_breaker),默认开启",
"最大输出未匹配 models.dev 时回填默认值 131072(128K 兜底)",
"修复 [watch] enabled 默认值显示(上游 #4015 翻回默认开启);「最大输出」列描述对齐上游 #4091",
],
},
{ {
version: "v0.8.0", version: "v0.8.0",
date: "2026-10-03", date: "2026-10-03",
@@ -532,6 +541,15 @@ const en: Dict = {
syncedNote: "Synced from GitHub Releases", syncedNote: "Synced from GitHub Releases",
fallbackNote: "Built-in release notes", fallbackNote: "Built-in release notes",
entries: [ entries: [
{
version: "v0.8.1",
date: "2026-10-04",
items: [
"Two new top-level switches in Global Settings: auto session title (auto_session_title) and repeat breaker (repeat_breaker), on by default",
"Max output backfills a 131072 (128K) default when models.dev has no match",
"Fixed the [watch] enabled default display (upstream #4015 flipped it back on); Max output column wording aligned with upstream #4091",
],
},
{ {
version: "v0.8.0", version: "v0.8.0",
date: "2026-10-03", date: "2026-10-03",
+1 -1
View File
@@ -8,7 +8,7 @@ const icons: Record<string, Icon> = {
linux: LinuxLogo, linux: LinuxLogo,
}; };
const VERSION = "0.8.0"; const VERSION = "0.8.1";
const GITHUB = "https://github.com/billowliu2/KimiSwitch"; const GITHUB = "https://github.com/billowliu2/KimiSwitch";
const MIRROR_RELEASES = "https://git.codingplan.site/admin/KimiCodeSwitch/releases"; const MIRROR_RELEASES = "https://git.codingplan.site/admin/KimiCodeSwitch/releases";
const dl = (file: string) => `${GITHUB}/releases/download/v${VERSION}/${file}`; const dl = (file: string) => `${GITHUB}/releases/download/v${VERSION}/${file}`;