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
90 lines
2.4 KiB
TypeScript
90 lines
2.4 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { useTranslation } from "../i18n";
|
|
import type { Agent, Config } from "../types";
|
|
|
|
interface UseConfigReturn {
|
|
config: Config | null;
|
|
dirty: boolean;
|
|
error: string | null;
|
|
loading: boolean;
|
|
refresh: () => Promise<void>;
|
|
save: () => Promise<void>;
|
|
updateConfig: (updater: (config: Config) => Config) => void;
|
|
}
|
|
|
|
export function useConfig(agent: Agent): UseConfigReturn {
|
|
const { t } = useTranslation();
|
|
const [config, setConfig] = useState<Config | null>(null);
|
|
const configRef = useRef(config);
|
|
const [dirty, setDirty] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
configRef.current = config;
|
|
}, [config]);
|
|
|
|
const refresh = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const loadedConfig = await invoke<Config>("load_agent_config_command", {
|
|
agent,
|
|
});
|
|
setConfig(loadedConfig);
|
|
setDirty(false);
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
setError(message);
|
|
console.error("useConfig refresh failed:", message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [agent]);
|
|
|
|
useEffect(() => {
|
|
refresh();
|
|
}, [refresh]);
|
|
|
|
const updateConfig = useCallback((updater: (config: Config) => Config) => {
|
|
// Compute next state synchronously from the ref (always current) and set
|
|
// the ref BEFORE setConfig. Previously the ref was updated inside the
|
|
// setConfig updater, which React 18 defers — so save() reading the ref
|
|
// immediately after would get a stale value.
|
|
const prev = configRef.current;
|
|
if (prev) {
|
|
const next = updater(prev);
|
|
configRef.current = next;
|
|
setConfig(next);
|
|
}
|
|
setDirty(true);
|
|
}, []);
|
|
|
|
const save = useCallback(async () => {
|
|
const current = configRef.current;
|
|
if (!current) return;
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await invoke("save_agent_config_command", { agent, config: current });
|
|
await refresh();
|
|
} catch (err) {
|
|
const raw = err instanceof Error ? err.message : String(err);
|
|
setError(raw);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [refresh, t, agent]);
|
|
|
|
return {
|
|
config,
|
|
dirty,
|
|
error,
|
|
loading,
|
|
refresh,
|
|
save,
|
|
updateConfig,
|
|
};
|
|
}
|