Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5138a295e0 | ||
|
|
a02179c9da | ||
|
|
86a128efea | ||
|
|
6a7c8f5393 |
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kimiswitch",
|
"name": "kimiswitch",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.11",
|
"version": "0.7.12",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# KimiSwitch v0.7.12
|
||||||
|
|
||||||
|
## 适配 kimi-code 0.40.1
|
||||||
|
|
||||||
|
- **实验 flag 清单更新(9→10)**:新增 `file_history`(轮级文件历史快照)、`search_worker`(kap-server 全局搜索 worker);`secondary-model`(子代理次主力模型)自 kimi-code 0.40.1 起默认开启,设置页显示"默认开启"徽章
|
||||||
|
- **flag 优先级语义修正**:跟随上游新优先级——单 flag 环境变量 > `[experimental]` 显式配置 > 总开关环境变量(仅强制开)> 默认值。总开关 `KIMI_CODE_EXPERIMENTAL_FLAG` 不再锁定全部开关,显式写入的 false/true 优先生效
|
||||||
|
- **环境变量探测补齐**:`tower` / `subagent_fork` / `wait_for` / `auto_session_title` 等此前漏探测的环境变量现在会正确显示"被环境变量锁定"状态
|
||||||
|
- **新增危险命令守卫开关**:权限设置页可配置 `[permission] dangerous_command_guard`(默认开启)——Auto 模式直接拒绝 `rm -rf` / `shutdown` 等危险命令,其它模式强制询问,关闭后回归旧行为
|
||||||
|
|
||||||
|
## 修复
|
||||||
|
|
||||||
|
- **WebUI 打开竞态**:首次打开较慢时多次点击"在应用内打开"不再报错——并发调用串行化 + 后到的点击自动聚焦已有窗口;失败路径不再误杀正在使用的 server;前端按钮增加"打开中..."防重入态
|
||||||
|
|
||||||
|
## 数据
|
||||||
|
|
||||||
|
- models-dev 快照更新至 7495 模型 / 212 供应商
|
||||||
Generated
+1
-1
@@ -1978,7 +1978,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.7.11"
|
version = "0.7.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.7.11"
|
version = "0.7.12"
|
||||||
description = "Kimi Switch - model config manager"
|
description = "Kimi Switch - model config manager"
|
||||||
authors = ["codingplan.site"]
|
authors = ["codingplan.site"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
+96
-35
@@ -1014,6 +1014,9 @@ pub struct KimiWebState {
|
|||||||
window: Mutex<Option<tauri::WebviewWindow>>,
|
window: Mutex<Option<tauri::WebviewWindow>>,
|
||||||
/// The `kimi web` child spawned by this app (None when reusing a server).
|
/// The `kimi web` child spawned by this app (None when reusing a server).
|
||||||
child: Mutex<Option<std::process::Child>>,
|
child: Mutex<Option<std::process::Child>>,
|
||||||
|
/// Serializes `open_kimi_web_embedded` invocations: a rapid double-click
|
||||||
|
/// queues here instead of racing the window/server setup.
|
||||||
|
launch_lock: tokio::sync::Mutex<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const KIMI_WEB_PORT: u16 = 58627;
|
const KIMI_WEB_PORT: u16 = 58627;
|
||||||
@@ -1064,7 +1067,13 @@ pub async fn open_kimi_web_embedded(
|
|||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
state: tauri::State<'_, KimiWebState>,
|
state: tauri::State<'_, KimiWebState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Singleton: focus the existing window instead of creating a second one.
|
// Serialize concurrent invocations (rapid double-click): a second call
|
||||||
|
// waits here until the first has finished building the window, then
|
||||||
|
// re-checks the singleton below and focuses it instead.
|
||||||
|
let _guard = state.launch_lock.lock().await;
|
||||||
|
|
||||||
|
// Singleton (re-checked after acquiring the lock): focus the existing
|
||||||
|
// window instead of creating a second one.
|
||||||
if let Ok(guard) = state.window.lock() {
|
if let Ok(guard) = state.window.lock() {
|
||||||
if let Some(w) = guard.as_ref() {
|
if let Some(w) = guard.as_ref() {
|
||||||
if w.is_visible().unwrap_or(false) {
|
if w.is_visible().unwrap_or(false) {
|
||||||
@@ -1075,8 +1084,21 @@ pub async fn open_kimi_web_embedded(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reuse an already-running server, otherwise spawn `kimi web --no-open`.
|
// Reuse the server this app spawned earlier (still running), or a server
|
||||||
if !kimi_web_alive(Duration::from_millis(800)).await {
|
// already running outside the app; only spawn when neither is present.
|
||||||
|
// `spawned_by_me` gates every failure-path kill, so a reused server is
|
||||||
|
// never terminated.
|
||||||
|
let mut spawned_by_me = false;
|
||||||
|
let mut child_running = false;
|
||||||
|
if let Ok(mut guard) = state.child.lock() {
|
||||||
|
// try_wait: Ok(None) = still running. Exited or unwaitable children
|
||||||
|
// are treated as gone so a fresh server is spawned below.
|
||||||
|
child_running = guard
|
||||||
|
.as_mut()
|
||||||
|
.map(|child| child.try_wait().map(|st| st.is_none()).unwrap_or(false))
|
||||||
|
.unwrap_or(false);
|
||||||
|
}
|
||||||
|
if !child_running && !kimi_web_alive(Duration::from_millis(800)).await {
|
||||||
let mut cmd = std::process::Command::new("kimi");
|
let mut cmd = std::process::Command::new("kimi");
|
||||||
cmd.args(["web", "--no-open", "--port", &KIMI_WEB_PORT.to_string()]);
|
cmd.args(["web", "--no-open", "--port", &KIMI_WEB_PORT.to_string()]);
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -1091,6 +1113,7 @@ pub async fn open_kimi_web_embedded(
|
|||||||
format!("failed to start `kimi web`: {e}")
|
format!("failed to start `kimi web`: {e}")
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
spawned_by_me = true;
|
||||||
if let Ok(mut guard) = state.child.lock() {
|
if let Ok(mut guard) = state.child.lock() {
|
||||||
*guard = Some(child);
|
*guard = Some(child);
|
||||||
}
|
}
|
||||||
@@ -1098,37 +1121,68 @@ pub async fn open_kimi_web_embedded(
|
|||||||
|
|
||||||
// Wait for the server to come up (fresh start takes a moment).
|
// Wait for the server to come up (fresh start takes a moment).
|
||||||
if !kimi_web_alive(Duration::from_secs(8)).await {
|
if !kimi_web_alive(Duration::from_secs(8)).await {
|
||||||
|
if spawned_by_me {
|
||||||
kill_spawned_kimi_web(&state);
|
kill_spawned_kimi_web(&state);
|
||||||
|
}
|
||||||
return Err(format!("kimi web did not come up within 8s ({KIMI_WEB_ORIGIN})"));
|
return Err(format!("kimi web did not come up within 8s ({KIMI_WEB_ORIGIN})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the window on the main thread (required on macOS).
|
// Build the URL, then create the window on the main thread (required on
|
||||||
|
// macOS). The build result is awaited while still holding `launch_lock`,
|
||||||
|
// so a concurrent second invocation blocks here and then lands on the
|
||||||
|
// singleton re-check above once the window exists.
|
||||||
let url: tauri::Url = match kimi_web_url().parse() {
|
let url: tauri::Url = match kimi_web_url().parse() {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(_) => return Err("invalid kimi web url".to_string()),
|
Err(_) => return Err("invalid kimi web url".to_string()),
|
||||||
};
|
};
|
||||||
let app = app.clone();
|
let (tx, rx) = tokio::sync::oneshot::channel::<Result<(), String>>();
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
let builder_app = app.clone();
|
let builder_app = app.clone();
|
||||||
let _ = app.run_on_main_thread(move || {
|
app.run_on_main_thread(move || {
|
||||||
match tauri::WebviewWindowBuilder::new(
|
let result = build_kimi_web_window(&builder_app, &url);
|
||||||
&builder_app,
|
let _ = tx.send(result);
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("failed to queue window creation: {e}"))?;
|
||||||
|
match rx.await {
|
||||||
|
Ok(result) => {
|
||||||
|
if let Err(e) = result {
|
||||||
|
// Window creation failed (e.g. label already taken): only stop
|
||||||
|
// the server when this invocation spawned it.
|
||||||
|
if spawned_by_me {
|
||||||
|
kill_spawned_kimi_web(&state);
|
||||||
|
}
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// Sender dropped without a result (app shutting down, build never
|
||||||
|
// ran): never kill the server — a later invocation may reuse it.
|
||||||
|
Err(_) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the embedded WebUI window on the main thread and track it for the
|
||||||
|
/// singleton check. On success, wires close/destroy events to forget the
|
||||||
|
/// window and stop the `kimi web` server this app spawned (reused servers are
|
||||||
|
/// untouched).
|
||||||
|
fn build_kimi_web_window(app: &tauri::AppHandle, url: &tauri::Url) -> Result<(), String> {
|
||||||
|
let window = tauri::WebviewWindowBuilder::new(
|
||||||
|
app,
|
||||||
KIMI_WEB_WINDOW_LABEL,
|
KIMI_WEB_WINDOW_LABEL,
|
||||||
tauri::WebviewUrl::External(url),
|
tauri::WebviewUrl::External(url.clone()),
|
||||||
)
|
)
|
||||||
.title("Kimi Code WebUI")
|
.title("Kimi Code WebUI")
|
||||||
.inner_size(1100.0, 750.0)
|
.inner_size(1100.0, 750.0)
|
||||||
.resizable(true)
|
.resizable(true)
|
||||||
.center()
|
.center()
|
||||||
.build()
|
.build()
|
||||||
{
|
.map_err(|e| format!("failed to create kimi web window: {e}"))?;
|
||||||
Ok(window) => {
|
|
||||||
// Track the window for the singleton check.
|
// Track the window for the singleton check.
|
||||||
if let Ok(mut guard) = builder_app.state::<KimiWebState>().window.lock() {
|
if let Ok(mut guard) = app.state::<KimiWebState>().window.lock() {
|
||||||
*guard = Some(window.clone());
|
*guard = Some(window.clone());
|
||||||
}
|
}
|
||||||
// Clean up when the window closes: forget it and stop the
|
// Clean up when the window closes: forget it and stop the `kimi web`
|
||||||
// `kimi web` server we spawned (reused servers are untouched).
|
// server we spawned (reused servers are untouched).
|
||||||
let w = window.clone();
|
let w = window.clone();
|
||||||
window.on_window_event(move |event| match event {
|
window.on_window_event(move |event| match event {
|
||||||
tauri::WindowEvent::Destroyed => {
|
tauri::WindowEvent::Destroyed => {
|
||||||
@@ -1141,15 +1195,6 @@ pub async fn open_kimi_web_embedded(
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// Window creation failed (e.g. rapid double-click racing
|
|
||||||
// the singleton check): don't leak the server we spawned.
|
|
||||||
kill_spawned_kimi_web(&builder_app.state::<KimiWebState>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
/// Preference for Linux is AppImage (portable, no install). If the preferred
|
/// Preference for Linux is AppImage (portable, no install). If the preferred
|
||||||
@@ -1294,26 +1339,42 @@ pub async fn kimi_oauth_poll(
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Read the Kimi Code environment variables that are currently set (non-empty)
|
/// Read the Kimi Code environment variables that are currently set (non-empty)
|
||||||
/// in this process's environment. The experimental-feature vars are used by the
|
/// in this process's environment. Every `[experimental]` feature env var is
|
||||||
/// frontend to mark config.toml `[experimental]` toggles as locked: env vars
|
/// probed — the frontend uses a per-feature env var to mark the matching
|
||||||
/// outrank the config file in Kimi Code's flag resolution. `KIMI_CODE_LEGACY_FLAG`
|
/// config.toml `[experimental]` toggle as locked, since a single-feature env
|
||||||
/// is probed too — the AgentSettingsPanel uses it to decide whether to
|
/// var outranks the config file in Kimi Code's flag resolution. The master
|
||||||
|
/// `KIMI_CODE_EXPERIMENTAL_FLAG` (0.40.1+: only force-ONs flags without an
|
||||||
|
/// explicit config entry; never locks a toggle) and `KIMI_CODE_LEGACY_FLAG`
|
||||||
|
/// are probed too — the latter lets AgentSettingsPanel decide whether to
|
||||||
/// dual-write the v1 engine's loop_control keys.
|
/// dual-write the v1 engine's loop_control keys.
|
||||||
///
|
///
|
||||||
|
/// The flag id → env var mapping mirrors `EXPERIMENTAL_FLAGS` in
|
||||||
|
/// src/lib/subagent-settings.ts; keep both lists in sync.
|
||||||
|
///
|
||||||
/// Truthy values per the CLI: `1` / `true` / `yes` / `on` (case-insensitive);
|
/// Truthy values per the CLI: `1` / `true` / `yes` / `on` (case-insensitive);
|
||||||
/// the raw values are returned and the truthy check happens on the frontend.
|
/// the raw values are returned and the truthy check happens on the frontend.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_experimental_env_status() -> HashMap<String, String> {
|
pub fn get_experimental_env_status() -> HashMap<String, String> {
|
||||||
const VARS: [&str; 6] = [
|
const VARS: [(&str, &str); 12] = [
|
||||||
"KIMI_CODE_EXPERIMENTAL_FLAG",
|
("secondary-model", "KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL"),
|
||||||
"KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL",
|
("tool-select", "KIMI_CODE_EXPERIMENTAL_TOOL_SELECT"),
|
||||||
"KIMI_CODE_EXPERIMENTAL_TOOL_SELECT",
|
(
|
||||||
|
"persistence_minidb_readmodel",
|
||||||
"KIMI_CODE_EXPERIMENTAL_PERSISTENCE_MINIDB_READMODEL",
|
"KIMI_CODE_EXPERIMENTAL_PERSISTENCE_MINIDB_READMODEL",
|
||||||
"KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL",
|
),
|
||||||
"KIMI_CODE_LEGACY_FLAG",
|
("tower", "KIMI_CODE_EXPERIMENTAL_TOWER"),
|
||||||
|
("subagent_fork", "KIMI_CODE_EXPERIMENTAL_SUBAGENT_FORK"),
|
||||||
|
("wait_for", "KIMI_CODE_EXPERIMENTAL_WAIT_FOR"),
|
||||||
|
("auto_session_title", "KIMI_CODE_EXPERIMENTAL_AUTO_SESSION_TITLE"),
|
||||||
|
("remote-control", "KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL"),
|
||||||
|
("search_worker", "KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER"),
|
||||||
|
("file_history", "KIMI_CODE_EXPERIMENTAL_FILE_HISTORY"),
|
||||||
|
// Non-flag probes consumed by the frontend:
|
||||||
|
("master", "KIMI_CODE_EXPERIMENTAL_FLAG"),
|
||||||
|
("legacy", "KIMI_CODE_LEGACY_FLAG"),
|
||||||
];
|
];
|
||||||
let mut out = HashMap::new();
|
let mut out = HashMap::new();
|
||||||
for name in VARS {
|
for (_, name) in VARS {
|
||||||
if let Ok(value) = std::env::var(name) {
|
if let Ok(value) = std::env::var(name) {
|
||||||
if !value.trim().is_empty() {
|
if !value.trim().is_empty() {
|
||||||
out.insert(name.to_string(), value);
|
out.insert(name.to_string(), value);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"productName": "Kimi Switch",
|
"productName": "Kimi Switch",
|
||||||
"version": "0.7.11",
|
"version": "0.7.12",
|
||||||
"identifier": "com.kimiswitch.app",
|
"identifier": "com.kimiswitch.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -157,6 +157,25 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title={t("permissionRules")}>
|
<Card title={t("permissionRules")}>
|
||||||
|
{/* kimi-code 0.40.1 `[permission] dangerous_command_guard`. The env
|
||||||
|
var KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false")
|
||||||
|
outranks this config at runtime; no env-lock UI here (deliberate
|
||||||
|
minimal scope). Absent key = upstream default on. */}
|
||||||
|
<Checkbox
|
||||||
|
label={t("permissionDangerousGuard")}
|
||||||
|
checked={settings.permission?.dangerous_command_guard ?? true}
|
||||||
|
onChange={(checked) =>
|
||||||
|
update({
|
||||||
|
permission: {
|
||||||
|
...settings.permission,
|
||||||
|
dangerous_command_guard: checked,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-content-muted">
|
||||||
|
{t("permissionDangerousGuardDesc")}
|
||||||
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{(settings.permission?.rules ?? []).map((rule, idx) => (
|
{(settings.permission?.rules ?? []).map((rule, idx) => (
|
||||||
<div key={idx} className="flex items-center gap-2">
|
<div key={idx} className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ const FLAG_LABELS: Record<string, { name: TranslationKey; desc: TranslationKey }
|
|||||||
desc: "flagAutoSessionTitleDesc",
|
desc: "flagAutoSessionTitleDesc",
|
||||||
},
|
},
|
||||||
"remote-control": { name: "flagRemoteControl", desc: "flagRemoteControlDesc" },
|
"remote-control": { name: "flagRemoteControl", desc: "flagRemoteControlDesc" },
|
||||||
|
search_worker: { name: "flagSearchWorker", desc: "flagSearchWorkerDesc" },
|
||||||
|
file_history: { name: "flagFileHistory", desc: "flagFileHistoryDesc" },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Validation-error i18n key per engine rule, for the pre-write self-check. */
|
/** Validation-error i18n key per engine rule, for the pre-write self-check. */
|
||||||
@@ -162,6 +164,8 @@ export function SubagentSettingsPage({
|
|||||||
const [poolErrors, setPoolErrors] = useState<PoolValidationError[]>([]);
|
const [poolErrors, setPoolErrors] = useState<PoolValidationError[]>([]);
|
||||||
/** Local draft for the "add pool entry" select (reset after each pick). */
|
/** Local draft for the "add pool entry" select (reset after each pick). */
|
||||||
const [addSelection, setAddSelection] = useState("");
|
const [addSelection, setAddSelection] = useState("");
|
||||||
|
/** WebUI-open button in flight; disables both buttons while non-null. */
|
||||||
|
const [webuiBusy, setWebuiBusy] = useState<"embedded" | "browser" | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invoke<ExperimentalEnvStatus>("get_experimental_env_status")
|
invoke<ExperimentalEnvStatus>("get_experimental_env_status")
|
||||||
@@ -185,13 +189,18 @@ export function SubagentSettingsPage({
|
|||||||
const secondaryFlag = EXPERIMENTAL_FLAGS[0]; // "secondary-model"
|
const secondaryFlag = EXPERIMENTAL_FLAGS[0]; // "secondary-model"
|
||||||
const otherFlags = EXPERIMENTAL_FLAGS.slice(1);
|
const otherFlags = EXPERIMENTAL_FLAGS.slice(1);
|
||||||
|
|
||||||
/** Effective state of the secondary-model flag (env overrides config). */
|
/** Effective state of the secondary-model flag. Mirrors the kimi-code
|
||||||
const secondaryEnabled = masterOn
|
* 0.40.1 priority: single-flag env > explicit [experimental] value >
|
||||||
? true
|
* master env (force-on only) > upstream default. The master env no
|
||||||
: isFlagLockedByEnv(env, secondaryFlag)
|
* longer locks the toggle — only the flag's own env var does. */
|
||||||
|
const secondaryEnabled = isFlagLockedByEnv(env, secondaryFlag)
|
||||||
? forcedEnvValue(env, secondaryFlag)
|
? forcedEnvValue(env, secondaryFlag)
|
||||||
: flags["secondary-model"] === true;
|
: isExperimentalFlagSet(rawOther, secondaryFlag.id)
|
||||||
const secondaryLocked = masterOn || isFlagLockedByEnv(env, secondaryFlag);
|
? flags["secondary-model"] === true
|
||||||
|
: masterOn
|
||||||
|
? true
|
||||||
|
: (secondaryFlag.defaultEnabled ?? false);
|
||||||
|
const secondaryLocked = isFlagLockedByEnv(env, secondaryFlag);
|
||||||
|
|
||||||
const aliasList = Object.keys(models).sort();
|
const aliasList = Object.keys(models).sort();
|
||||||
|
|
||||||
@@ -300,17 +309,24 @@ export function SubagentSettingsPage({
|
|||||||
|
|
||||||
const renderFlagRow = (flag: ExperimentalFlagDef) => {
|
const renderFlagRow = (flag: ExperimentalFlagDef) => {
|
||||||
const labels = FLAG_LABELS[flag.id];
|
const labels = FLAG_LABELS[flag.id];
|
||||||
const locked = masterOn || isFlagLockedByEnv(env, flag);
|
// Only the flag's own env var locks the toggle. The master env force-ons
|
||||||
// An explicitly written flag (true or false) wins; an absent flag falls
|
// undefined flags but an explicit config entry outranks it, so the user
|
||||||
// back to the upstream default (`defaultEnabled`).
|
// can still flip values here while it is set.
|
||||||
|
const locked = isFlagLockedByEnv(env, flag);
|
||||||
|
// Resolution order mirrors kimi-code 0.40.1 flagService:
|
||||||
|
// single-flag env > explicit [experimental] value (true *or* false) >
|
||||||
|
// master env (on-only) > upstream default (`defaultEnabled`).
|
||||||
const explicitlySet = isExperimentalFlagSet(rawOther, flag.id);
|
const explicitlySet = isExperimentalFlagSet(rawOther, flag.id);
|
||||||
const on = masterOn
|
const on = locked
|
||||||
? true
|
|
||||||
: isFlagLockedByEnv(env, flag)
|
|
||||||
? forcedEnvValue(env, flag)
|
? forcedEnvValue(env, flag)
|
||||||
: explicitlySet
|
: explicitlySet
|
||||||
? flags[flag.id] === true
|
? flags[flag.id] === true
|
||||||
|
: masterOn
|
||||||
|
? true
|
||||||
: (flag.defaultEnabled ?? false);
|
: (flag.defaultEnabled ?? false);
|
||||||
|
// Turning the flag off only sticks via an explicit `false` when the
|
||||||
|
// fallback (master env or the upstream default) would otherwise keep it on.
|
||||||
|
const explicitFalse = masterOn || flag.defaultEnabled === true;
|
||||||
return (
|
return (
|
||||||
<div key={flag.id} className="flex items-center gap-3">
|
<div key={flag.id} className="flex items-center gap-3">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -337,7 +353,9 @@ export function SubagentSettingsPage({
|
|||||||
disabled={locked}
|
disabled={locked}
|
||||||
ariaLabel={t(labels.name)}
|
ariaLabel={t(labels.name)}
|
||||||
onChange={(checked) =>
|
onChange={(checked) =>
|
||||||
onChange(setExperimentalFlag(rawOther, flag.id, checked))
|
onChange(
|
||||||
|
setExperimentalFlag(rawOther, flag.id, checked, { explicitFalse })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -371,29 +389,37 @@ export function SubagentSettingsPage({
|
|||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={webuiBusy !== null}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
setWebuiBusy("embedded");
|
||||||
try {
|
try {
|
||||||
await invoke("open_kimi_web_embedded");
|
await invoke("open_kimi_web_embedded");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err instanceof Error ? err.message : String(err));
|
alert(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setWebuiBusy(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1.5 text-sm rounded bg-blue-600 text-white hover:bg-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
className="px-3 py-1.5 text-sm rounded bg-blue-600 text-white hover:bg-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{t("openWebUIEmbedded")}
|
{webuiBusy === "embedded" ? t("webuiOpening") : t("openWebUIEmbedded")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={webuiBusy !== null}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
setWebuiBusy("browser");
|
||||||
try {
|
try {
|
||||||
await invoke("open_kimi_web");
|
await invoke("open_kimi_web");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err instanceof Error ? err.message : String(err));
|
alert(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setWebuiBusy(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{t("openWebUIBrowser")}
|
{webuiBusy === "browser" ? t("webuiOpening") : t("openWebUIBrowser")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -550,6 +576,16 @@ export function SubagentSettingsPage({
|
|||||||
<div className="border-t border-border" />
|
<div className="border-t border-border" />
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-content-primary">
|
||||||
|
{t(FLAG_LABELS[secondaryFlag.id].name)}
|
||||||
|
{!secondaryLocked &&
|
||||||
|
!isExperimentalFlagSet(rawOther, secondaryFlag.id) &&
|
||||||
|
secondaryFlag.defaultEnabled && (
|
||||||
|
<span className="shrink-0 text-xs text-blue-600 dark:text-blue-400 border border-blue-500/30 rounded px-1">
|
||||||
|
{t("flagDefaultOn")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="text-xs text-content-muted">
|
<div className="text-xs text-content-muted">
|
||||||
{t(FLAG_LABELS[secondaryFlag.id].desc)}
|
{t(FLAG_LABELS[secondaryFlag.id].desc)}
|
||||||
<code className="ml-2">{secondaryFlag.envVar}</code>
|
<code className="ml-2">{secondaryFlag.envVar}</code>
|
||||||
@@ -565,7 +601,11 @@ export function SubagentSettingsPage({
|
|||||||
disabled={secondaryLocked}
|
disabled={secondaryLocked}
|
||||||
ariaLabel={t(FLAG_LABELS[secondaryFlag.id].name)}
|
ariaLabel={t(FLAG_LABELS[secondaryFlag.id].name)}
|
||||||
onChange={(checked) =>
|
onChange={(checked) =>
|
||||||
onChange(setExperimentalFlag(rawOther, secondaryFlag.id, checked))
|
onChange(
|
||||||
|
setExperimentalFlag(rawOther, secondaryFlag.id, checked, {
|
||||||
|
explicitFalse: masterOn || secondaryFlag.defaultEnabled === true,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+11
-1
@@ -151,6 +151,9 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
permissionAllow: "Allow",
|
permissionAllow: "Allow",
|
||||||
permissionDeny: "Deny",
|
permissionDeny: "Deny",
|
||||||
permissionAsk: "Ask",
|
permissionAsk: "Ask",
|
||||||
|
permissionDangerousGuard: "Dangerous Command Guard",
|
||||||
|
permissionDangerousGuardDesc:
|
||||||
|
"When on (default): Auto mode refuses dangerous commands (rm -rf, shutdown, dd of=, ...) and other modes always prompt; turning it off restores the previous behavior. Env KIMI_CODE_DANGEROUS_COMMAND_GUARD overrides this config",
|
||||||
addRule: "+ Add rule",
|
addRule: "+ Add rule",
|
||||||
addCommonRules: "Add common rules",
|
addCommonRules: "Add common rules",
|
||||||
hooks: "Lifecycle Hooks",
|
hooks: "Lifecycle Hooks",
|
||||||
@@ -477,9 +480,15 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
flagAutoSessionTitleDesc: "Generate session titles automatically via the managed chat_title tool",
|
flagAutoSessionTitleDesc: "Generate session titles automatically via the managed chat_title tool",
|
||||||
flagRemoteControl: "Remote Control",
|
flagRemoteControl: "Remote Control",
|
||||||
flagRemoteControlDesc: "Enable the experimental remote control feature",
|
flagRemoteControlDesc: "Enable the experimental remote control feature",
|
||||||
|
flagSearchWorker: "Search Worker",
|
||||||
|
flagSearchWorkerDesc: "Run the kap-server global search backend in a background worker host",
|
||||||
|
flagFileHistory: "File History",
|
||||||
|
flagFileHistoryDesc:
|
||||||
|
"Snapshot edited files before/after each turn (last 5 turns across 30 sessions, stored in the session data dir) for real whole-file diffs",
|
||||||
flagDefaultOn: "On by default",
|
flagDefaultOn: "On by default",
|
||||||
lockedByEnv: "Locked by env var",
|
lockedByEnv: "Locked by env var",
|
||||||
masterEnvOnHint: "KIMI_CODE_EXPERIMENTAL_FLAG is set: all experimental features are forced on by the env var",
|
masterEnvOnHint:
|
||||||
|
"KIMI_CODE_EXPERIMENTAL_FLAG is set: flags without an explicit [experimental] entry are forced on; explicit entries in config.toml still win",
|
||||||
secondaryModelSection: "Subagent Model (Secondary Model)",
|
secondaryModelSection: "Subagent Model (Secondary Model)",
|
||||||
secondaryModelDisabledHint:
|
secondaryModelDisabledHint:
|
||||||
"The experimental flag \"Subagent Secondary Model\" is off and this config will not take effect — enable the toggle above first",
|
"The experimental flag \"Subagent Secondary Model\" is off and this config will not take effect — enable the toggle above first",
|
||||||
@@ -528,6 +537,7 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
"Open the new Web UI in an independent in-app window, or in your system browser (built into kimi-code 0.33+). Requires the kimi CLI on PATH.",
|
"Open the new Web UI in an independent in-app window, or in your system browser (built into kimi-code 0.33+). Requires the kimi CLI on PATH.",
|
||||||
openWebUIEmbedded: "Open in App",
|
openWebUIEmbedded: "Open in App",
|
||||||
openWebUIBrowser: "Open in Browser",
|
openWebUIBrowser: "Open in Browser",
|
||||||
|
webuiOpening: "Opening...",
|
||||||
|
|
||||||
// Plugin marketplace
|
// Plugin marketplace
|
||||||
pluginMarketplace: "Plugin Marketplace",
|
pluginMarketplace: "Plugin Marketplace",
|
||||||
|
|||||||
+11
-1
@@ -149,6 +149,9 @@ export const zhTranslations = {
|
|||||||
permissionAllow: "允许",
|
permissionAllow: "允许",
|
||||||
permissionDeny: "拒绝",
|
permissionDeny: "拒绝",
|
||||||
permissionAsk: "询问",
|
permissionAsk: "询问",
|
||||||
|
permissionDangerousGuard: "危险命令防护",
|
||||||
|
permissionDangerousGuardDesc:
|
||||||
|
"开启(默认)时:Auto 模式直接拒绝危险命令(rm -rf、shutdown、dd of= 等),其它模式强制询问;关闭后回归旧行为。环境变量 KIMI_CODE_DANGEROUS_COMMAND_GUARD 会覆盖本配置",
|
||||||
addRule: "+ 添加规则",
|
addRule: "+ 添加规则",
|
||||||
addCommonRules: "添加常用规则",
|
addCommonRules: "添加常用规则",
|
||||||
hooks: "生命周期钩子",
|
hooks: "生命周期钩子",
|
||||||
@@ -472,9 +475,15 @@ export const zhTranslations = {
|
|||||||
flagAutoSessionTitleDesc: "通过托管 chat_title 工具自动生成会话标题",
|
flagAutoSessionTitleDesc: "通过托管 chat_title 工具自动生成会话标题",
|
||||||
flagRemoteControl: "远程控制",
|
flagRemoteControl: "远程控制",
|
||||||
flagRemoteControlDesc: "启用实验性远程控制功能",
|
flagRemoteControlDesc: "启用实验性远程控制功能",
|
||||||
|
flagSearchWorker: "搜索 Worker",
|
||||||
|
flagSearchWorkerDesc: "kap-server 全局搜索的后台 worker 宿主服务",
|
||||||
|
flagFileHistory: "文件历史",
|
||||||
|
flagFileHistoryDesc:
|
||||||
|
"记录每轮编辑文件的前后快照(最近 30 个会话×最近 5 轮,存于会话数据目录),用于真实的整文件 diff",
|
||||||
flagDefaultOn: "默认开启",
|
flagDefaultOn: "默认开启",
|
||||||
lockedByEnv: "被环境变量锁定",
|
lockedByEnv: "被环境变量锁定",
|
||||||
masterEnvOnHint: "KIMI_CODE_EXPERIMENTAL_FLAG 已设置:全部实验功能被环境变量强制开启",
|
masterEnvOnHint:
|
||||||
|
"KIMI_CODE_EXPERIMENTAL_FLAG 已设置:未显式配置的实验项将被强制开启;config.toml 中的显式配置优先生效",
|
||||||
secondaryModelSection: "子代理模型(次主力模型)",
|
secondaryModelSection: "子代理模型(次主力模型)",
|
||||||
secondaryModelDisabledHint:
|
secondaryModelDisabledHint:
|
||||||
"实验开关「子代理次主力模型」未启用,本配置不会生效——请先在上方打开对应开关",
|
"实验开关「子代理次主力模型」未启用,本配置不会生效——请先在上方打开对应开关",
|
||||||
@@ -520,6 +529,7 @@ export const zhTranslations = {
|
|||||||
"以独立窗口在应用内打开新版 Web 界面,也可以选择在系统浏览器打开(kimi-code 0.33+ 内置 code-app;需要 kimi 命令在 PATH 中)。",
|
"以独立窗口在应用内打开新版 Web 界面,也可以选择在系统浏览器打开(kimi-code 0.33+ 内置 code-app;需要 kimi 命令在 PATH 中)。",
|
||||||
openWebUIEmbedded: "在应用内打开",
|
openWebUIEmbedded: "在应用内打开",
|
||||||
openWebUIBrowser: "在浏览器打开",
|
openWebUIBrowser: "在浏览器打开",
|
||||||
|
webuiOpening: "打开中...",
|
||||||
|
|
||||||
// Plugin marketplace
|
// Plugin marketplace
|
||||||
pluginMarketplace: "插件市场",
|
pluginMarketplace: "插件市场",
|
||||||
|
|||||||
@@ -132,3 +132,75 @@ describe("loop_control — v1/v2 key handling", () => {
|
|||||||
expect(s.loop_control?.max_retries_per_step).toBe(4);
|
expect(s.loop_control?.max_retries_per_step).toBe(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// [permission] dangerous_command_guard (kimi-code 0.40.1) — absent key means
|
||||||
|
// upstream default ON; an explicit false must survive even with no rules
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("permission.dangerous_command_guard", () => {
|
||||||
|
it("reads as undefined when the key is absent (default on)", () => {
|
||||||
|
expect(
|
||||||
|
getAgentSettings({}).permission?.dangerous_command_guard
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
getAgentSettings({ permission: { rules: [] } }).permission
|
||||||
|
?.dangerous_command_guard
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads an explicit true / false", () => {
|
||||||
|
expect(
|
||||||
|
getAgentSettings({ permission: { dangerous_command_guard: false } })
|
||||||
|
.permission?.dangerous_command_guard
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
getAgentSettings({ permission: { dangerous_command_guard: true } })
|
||||||
|
.permission?.dangerous_command_guard
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes guard=false even with no rules (section kept)", () => {
|
||||||
|
const next = setAgentSettings(
|
||||||
|
{},
|
||||||
|
{ permission: { rules: [], dangerous_command_guard: false } }
|
||||||
|
) as { permission?: Record<string, unknown> };
|
||||||
|
expect(next.permission).toEqual({ dangerous_command_guard: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes guard together with rules", () => {
|
||||||
|
const rules = [{ decision: "allow" as const, pattern: "Read" }];
|
||||||
|
const next = setAgentSettings(
|
||||||
|
{},
|
||||||
|
{ permission: { rules, dangerous_command_guard: true } }
|
||||||
|
) as { permission?: Record<string, unknown> };
|
||||||
|
expect(next.permission).toEqual({
|
||||||
|
rules,
|
||||||
|
dangerous_command_guard: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a rules-only update carries the existing guard through", () => {
|
||||||
|
const raw = { permission: { dangerous_command_guard: false } };
|
||||||
|
const next = setAgentSettings(raw, {
|
||||||
|
permission: { rules: [{ decision: "deny" as const, pattern: "Bash" }] },
|
||||||
|
}) as { permission?: Record<string, unknown> };
|
||||||
|
expect(next.permission?.dangerous_command_guard).toBe(false);
|
||||||
|
expect(next.permission?.rules).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits an empty rules array; keeps an explicit guard on plain saves", () => {
|
||||||
|
const raw = { permission: { dangerous_command_guard: true } };
|
||||||
|
const off = setAgentSettings(raw, {
|
||||||
|
permission: { rules: [], dangerous_command_guard: false },
|
||||||
|
}) as { permission?: Record<string, unknown> };
|
||||||
|
expect(off.permission).toEqual({ dangerous_command_guard: false });
|
||||||
|
expect(off.permission).not.toHaveProperty("rules");
|
||||||
|
// An explicit true is materialized on plain saves (harmless, and it
|
||||||
|
// documents the state the UI toggle shows).
|
||||||
|
const on = setAgentSettings(raw, {}) as {
|
||||||
|
permission?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
expect(on.permission).toEqual({ dangerous_command_guard: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -65,6 +65,10 @@ export function getAgentSettings(rawOther: unknown): AgentSettings {
|
|||||||
if (thinking.effort === "max") {
|
if (thinking.effort === "max") {
|
||||||
thinking.effort = "high";
|
thinking.effort = "high";
|
||||||
}
|
}
|
||||||
|
const sectionPermission = getSection<AgentSettings["permission"]>(
|
||||||
|
rawOther,
|
||||||
|
"permission"
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
thinking,
|
thinking,
|
||||||
loop_control: loop,
|
loop_control: loop,
|
||||||
@@ -73,9 +77,15 @@ export function getAgentSettings(rawOther: unknown): AgentSettings {
|
|||||||
...getSection<AgentSettings["background"]>(rawOther, "background"),
|
...getSection<AgentSettings["background"]>(rawOther, "background"),
|
||||||
},
|
},
|
||||||
permission: {
|
permission: {
|
||||||
rules:
|
rules: sectionPermission?.rules ?? [],
|
||||||
getSection<AgentSettings["permission"]>(rawOther, "permission")?.rules ??
|
// `dangerous_command_guard` stays undefined unless the config carries
|
||||||
[],
|
// an explicit boolean — upstream defaults it to ON (kimi-code 0.40.1),
|
||||||
|
// so an absent key means "on". The env var
|
||||||
|
// KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false" only)
|
||||||
|
// outranks this config value at kimi-code runtime.
|
||||||
|
...(typeof sectionPermission?.dangerous_command_guard === "boolean"
|
||||||
|
? { dangerous_command_guard: sectionPermission.dangerous_command_guard }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [],
|
hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [],
|
||||||
};
|
};
|
||||||
@@ -88,13 +98,20 @@ export function setAgentSettings(
|
|||||||
): unknown {
|
): unknown {
|
||||||
const root = { ...asRecord(rawOther) };
|
const root = { ...asRecord(rawOther) };
|
||||||
const current = getAgentSettings(rawOther);
|
const current = getAgentSettings(rawOther);
|
||||||
|
// Explicit guard value wins over the carried-over one; undefined keeps the
|
||||||
|
// key absent (upstream default = on).
|
||||||
|
const guard =
|
||||||
|
patch.permission?.dangerous_command_guard ??
|
||||||
|
current.permission?.dangerous_command_guard;
|
||||||
|
const permission: NonNullable<AgentSettings["permission"]> = {
|
||||||
|
rules: patch.permission?.rules ?? current.permission?.rules ?? [],
|
||||||
|
...(typeof guard === "boolean" ? { dangerous_command_guard: guard } : {}),
|
||||||
|
};
|
||||||
const next: AgentSettings = {
|
const next: AgentSettings = {
|
||||||
thinking: { ...current.thinking, ...patch.thinking },
|
thinking: { ...current.thinking, ...patch.thinking },
|
||||||
loop_control: { ...current.loop_control, ...patch.loop_control },
|
loop_control: { ...current.loop_control, ...patch.loop_control },
|
||||||
background: { ...current.background, ...patch.background },
|
background: { ...current.background, ...patch.background },
|
||||||
permission: {
|
permission,
|
||||||
rules: patch.permission?.rules ?? current.permission?.rules ?? [],
|
|
||||||
},
|
|
||||||
hooks: patch.hooks ?? current.hooks ?? [],
|
hooks: patch.hooks ?? current.hooks ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,8 +132,19 @@ export function setAgentSettings(
|
|||||||
if (next.thinking) root.thinking = next.thinking;
|
if (next.thinking) root.thinking = next.thinking;
|
||||||
if (next.loop_control) root.loop_control = next.loop_control;
|
if (next.loop_control) root.loop_control = next.loop_control;
|
||||||
if (next.background) root.background = next.background;
|
if (next.background) root.background = next.background;
|
||||||
if (next.permission?.rules && next.permission.rules.length > 0) {
|
// Keep `[permission]` when it carries rules *or* an explicit
|
||||||
root.permission = next.permission;
|
// dangerous_command_guard — dropping the section would silently lose a
|
||||||
|
// guard=false write (absent key = upstream default on). An empty rules
|
||||||
|
// array is omitted rather than written as `rules = []`.
|
||||||
|
const hasRules = !!permission.rules && permission.rules.length > 0;
|
||||||
|
const hasGuard = typeof permission.dangerous_command_guard === "boolean";
|
||||||
|
if (hasRules || hasGuard) {
|
||||||
|
root.permission = {
|
||||||
|
...(hasRules ? { rules: permission.rules } : {}),
|
||||||
|
...(hasGuard
|
||||||
|
? { dangerous_command_guard: permission.dangerous_command_guard }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
delete root.permission;
|
delete root.permission;
|
||||||
}
|
}
|
||||||
|
|||||||
+3208
-2787
File diff suppressed because it is too large.
Load diff
+2614
-2213
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,12 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
EXPERIMENTAL_FLAGS,
|
||||||
|
getExperimentalFlags,
|
||||||
getSecondaryModel,
|
getSecondaryModel,
|
||||||
getSubagentModelPool,
|
getSubagentModelPool,
|
||||||
|
isExperimentalFlagSet,
|
||||||
removeSubagentPoolEntry,
|
removeSubagentPoolEntry,
|
||||||
|
setExperimentalFlag,
|
||||||
setSecondaryModelOnly,
|
setSecondaryModelOnly,
|
||||||
setSubagentDefault,
|
setSubagentDefault,
|
||||||
setSubagentForce,
|
setSubagentForce,
|
||||||
@@ -449,3 +453,76 @@ describe("validateSubagentPool", () => {
|
|||||||
expect(errors).toContainEqual({ key: "forceExcludesModels" });
|
expect(errors).toContainEqual({ key: "forceExcludesModels" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// [experimental] — flag registry (kimi-code 0.40.1) + write semantics
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("experimental flag registry", () => {
|
||||||
|
it("mirrors the 0.40.1 v2 registry (10 flags incl. file_history)", () => {
|
||||||
|
expect(EXPERIMENTAL_FLAGS.map((f) => f.id)).toEqual([
|
||||||
|
"secondary-model",
|
||||||
|
"tool-select",
|
||||||
|
"persistence_minidb_readmodel",
|
||||||
|
"tower",
|
||||||
|
"subagent_fork",
|
||||||
|
"wait_for",
|
||||||
|
"auto_session_title",
|
||||||
|
"remote-control",
|
||||||
|
"search_worker",
|
||||||
|
"file_history",
|
||||||
|
]);
|
||||||
|
const fh = EXPERIMENTAL_FLAGS.find((f) => f.id === "file_history");
|
||||||
|
expect(fh?.envVar).toBe("KIMI_CODE_EXPERIMENTAL_FILE_HISTORY");
|
||||||
|
expect(fh?.defaultEnabled).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("secondary-model is on by default since 0.40.1", () => {
|
||||||
|
expect(
|
||||||
|
EXPERIMENTAL_FLAGS.find((f) => f.id === "secondary-model")?.defaultEnabled
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setExperimentalFlag", () => {
|
||||||
|
const rawExp = (section: Record<string, unknown>) => ({
|
||||||
|
experimental: section,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enabling writes true; disabling a default-off flag deletes the key", () => {
|
||||||
|
const on = setExperimentalFlag({}, "tool-select", true) as {
|
||||||
|
experimental: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
expect(on.experimental["tool-select"]).toBe(true);
|
||||||
|
const off = setExperimentalFlag(on, "tool-select", false) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
expect(off).not.toHaveProperty("experimental"); // empty section dropped
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disabling a default-on flag with explicitFalse writes a literal false", () => {
|
||||||
|
const next = setExperimentalFlag(
|
||||||
|
rawExp({ wait_for: true }),
|
||||||
|
"wait_for",
|
||||||
|
false,
|
||||||
|
{ explicitFalse: true }
|
||||||
|
) as { experimental: Record<string, unknown> };
|
||||||
|
expect(next.experimental.wait_for).toBe(false);
|
||||||
|
expect(isExperimentalFlagSet(next, "wait_for")).toBe(true);
|
||||||
|
expect(getExperimentalFlags(next).wait_for).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("an explicit false coexists with other flags and keeps the section", () => {
|
||||||
|
const next = setExperimentalFlag(
|
||||||
|
rawExp({ "secondary-model": false }),
|
||||||
|
"tower",
|
||||||
|
true,
|
||||||
|
{ explicitFalse: true }
|
||||||
|
) as { experimental: Record<string, unknown> };
|
||||||
|
expect(next.experimental).toEqual({
|
||||||
|
"secondary-model": false,
|
||||||
|
tower: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -36,10 +36,16 @@ export interface ExperimentalFlagDef {
|
|||||||
/** Known experimental flags — mirrors the kimi-code v2 flag registry
|
/** Known experimental flags — mirrors the kimi-code v2 flag registry
|
||||||
* (the per-feature flag.ts files under packages/agent-core-v2/src:
|
* (the per-feature flag.ts files under packages/agent-core-v2/src:
|
||||||
* secondary-model, tool-select, persistence_minidb_readmodel, tower,
|
* secondary-model, tool-select, persistence_minidb_readmodel, tower,
|
||||||
* subagent_fork, wait_for, auto_session_title, remote-control). `acp-v2` was
|
* subagent_fork, wait_for, auto_session_title, remote-control,
|
||||||
* removed upstream and is dropped here. */
|
* search_worker, file_history). `acp-v2` was removed upstream and is
|
||||||
|
* dropped here. Keep the env-var list in sync with
|
||||||
|
* `get_experimental_env_status` in src-tauri/src/commands.rs. */
|
||||||
export const EXPERIMENTAL_FLAGS: ExperimentalFlagDef[] = [
|
export const EXPERIMENTAL_FLAGS: ExperimentalFlagDef[] = [
|
||||||
{ id: "secondary-model", envVar: "KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL" },
|
{
|
||||||
|
id: "secondary-model",
|
||||||
|
envVar: "KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL",
|
||||||
|
defaultEnabled: true, // on by default since kimi-code 0.40.1
|
||||||
|
},
|
||||||
{ id: "tool-select", envVar: "KIMI_CODE_EXPERIMENTAL_TOOL_SELECT" },
|
{ id: "tool-select", envVar: "KIMI_CODE_EXPERIMENTAL_TOOL_SELECT" },
|
||||||
{
|
{
|
||||||
id: "persistence_minidb_readmodel",
|
id: "persistence_minidb_readmodel",
|
||||||
@@ -58,11 +64,21 @@ export const EXPERIMENTAL_FLAGS: ExperimentalFlagDef[] = [
|
|||||||
envVar: "KIMI_CODE_EXPERIMENTAL_AUTO_SESSION_TITLE",
|
envVar: "KIMI_CODE_EXPERIMENTAL_AUTO_SESSION_TITLE",
|
||||||
},
|
},
|
||||||
{ id: "remote-control", envVar: "KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL" },
|
{ id: "remote-control", envVar: "KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL" },
|
||||||
|
{
|
||||||
|
id: "search_worker",
|
||||||
|
envVar: "KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER",
|
||||||
|
defaultEnabled: true,
|
||||||
|
},
|
||||||
|
{ id: "file_history", envVar: "KIMI_CODE_EXPERIMENTAL_FILE_HISTORY" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EXPERIMENTAL_MASTER_ENV = "KIMI_CODE_EXPERIMENTAL_FLAG";
|
export const EXPERIMENTAL_MASTER_ENV = "KIMI_CODE_EXPERIMENTAL_FLAG";
|
||||||
|
|
||||||
/** Whether the master env var is set to a truthy value (locks every flag on). */
|
/** Whether the master env var is set to a truthy value. Since kimi-code
|
||||||
|
* 0.40.1 it only force-ONs flags that have *no* explicit `[experimental]`
|
||||||
|
* entry (an explicit config value — true or false — outranks it, and the
|
||||||
|
* master env can never force a flag off). It locks no UI toggles; only a
|
||||||
|
* flag's own single-feature env var does. */
|
||||||
export function isMasterEnvOn(env: ExperimentalEnvStatus | null): boolean {
|
export function isMasterEnvOn(env: ExperimentalEnvStatus | null): boolean {
|
||||||
return isTruthyEnv(env?.[EXPERIMENTAL_MASTER_ENV]);
|
return isTruthyEnv(env?.[EXPERIMENTAL_MASTER_ENV]);
|
||||||
}
|
}
|
||||||
@@ -105,16 +121,24 @@ export function isExperimentalFlagSet(rawOther: unknown, id: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set one flag in `[experimental]`. Removing all flags drops the section. */
|
/** Set one flag in `[experimental]`. Removing all flags drops the section.
|
||||||
|
* Pass `explicitFalse` when deleting the key would NOT express "off" —
|
||||||
|
* i.e. the flag is on by upstream default (`defaultEnabled`) or the master
|
||||||
|
* env force-ons undefined flags — and write a literal `false` instead,
|
||||||
|
* which outranks both under the kimi-code 0.40.1 priority
|
||||||
|
* (single env > explicit config > master env > default). */
|
||||||
export function setExperimentalFlag(
|
export function setExperimentalFlag(
|
||||||
rawOther: unknown,
|
rawOther: unknown,
|
||||||
id: string,
|
id: string,
|
||||||
enabled: boolean
|
enabled: boolean,
|
||||||
|
opts?: { explicitFalse?: boolean }
|
||||||
): unknown {
|
): unknown {
|
||||||
const root = { ...asRecord(rawOther) };
|
const root = { ...asRecord(rawOther) };
|
||||||
const flags = { ...getExperimentalFlags(rawOther) };
|
const flags = { ...getExperimentalFlags(rawOther) };
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
flags[id] = true;
|
flags[id] = true;
|
||||||
|
} else if (opts?.explicitFalse) {
|
||||||
|
flags[id] = false;
|
||||||
} else {
|
} else {
|
||||||
delete flags[id];
|
delete flags[id];
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -149,7 +149,15 @@ export interface AgentSettings {
|
|||||||
thinking?: ThinkingConfig;
|
thinking?: ThinkingConfig;
|
||||||
loop_control?: LoopControlConfig;
|
loop_control?: LoopControlConfig;
|
||||||
background?: BackgroundConfig;
|
background?: BackgroundConfig;
|
||||||
permission?: { rules?: PermissionRule[] };
|
permission?: {
|
||||||
|
rules?: PermissionRule[];
|
||||||
|
/** kimi-code 0.40.1 `[permission]` key. Default true when the key is
|
||||||
|
* absent: Auto mode refuses dangerous commands (rm -rf, shutdown,
|
||||||
|
* dd of=, …) and other modes always prompt; false restores the
|
||||||
|
* previous behavior. Env KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal
|
||||||
|
* "true"/"false" only) outranks this config. */
|
||||||
|
dangerous_command_guard?: boolean;
|
||||||
|
};
|
||||||
hooks?: Hook[];
|
hooks?: Hook[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user