Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
138a2b8547 | ||
|
|
769852c4b5 |
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kimiswitch",
|
"name": "kimiswitch",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.7",
|
"version": "0.7.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
## v0.7.8
|
||||||
|
|
||||||
|
### 修复:模型用量上下显示不同步
|
||||||
|
|
||||||
|
- 修复同一供应商卡片上,上方用量摘要(含 🔄 刷新按钮)刷新后、下方用量进度条仍停留在旧数据的问题(如上方 24% vs 下方 8%)
|
||||||
|
- 用量查询状态提升至卡片层统一持有:点刷新、自动轮询后,两处显示同帧同步更新
|
||||||
|
- 自动刷新间隔不再只挂在其中一处显示上,每卡片恰好一份定时器,行为不变
|
||||||
|
|
||||||
|
### 实验功能开关同步 kimi-code 最新旗标注册表
|
||||||
|
|
||||||
|
- 高级设置的实验功能清单对齐 kimi-code 上游 v2 旗标注册表(7 项)
|
||||||
|
- 新增开关:**Tower 模式**(多智能体协同,/tower 命令切换)、**子代理 Fork 上下文**(Agent/AgentSwarm 携带主代理会话快照)、**WaitFor 工具**(回合内等待后台任务)、**自动会话标题**(托管 chat_title 工具)
|
||||||
|
- 移除上游已废弃的 **ACP v2 协议** 开关(手写进 config.toml 的配置仍会完整保留)
|
||||||
Generated
+1
-1
@@ -1978,7 +1978,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.7.7"
|
version = "0.7.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.7.7"
|
version = "0.7.8"
|
||||||
description = "Kimi Switch - model config manager"
|
description = "Kimi Switch - model config manager"
|
||||||
authors = ["codingplan.site"]
|
authors = ["codingplan.site"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"productName": "Kimi Switch",
|
"productName": "Kimi Switch",
|
||||||
"version": "0.7.7",
|
"version": "0.7.8",
|
||||||
"identifier": "com.kimiswitch.app",
|
"identifier": "com.kimiswitch.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
+239
-199
@@ -4,6 +4,7 @@ import { Pencil, Copy, Activity, Loader2, Trash2, BarChart3 } from "lucide-react
|
|||||||
import { useTranslation } from "../i18n";
|
import { useTranslation } from "../i18n";
|
||||||
import { ProviderIcon } from "./ProviderIcon";
|
import { ProviderIcon } from "./ProviderIcon";
|
||||||
import { UsageFooter } from "./UsageFooter";
|
import { UsageFooter } from "./UsageFooter";
|
||||||
|
import { useUsageQuery } from "../hooks/useUsageQuery";
|
||||||
import type { Agent, Model, Provider } from "../types";
|
import type { Agent, Model, Provider } from "../types";
|
||||||
|
|
||||||
interface ConnectivityResult {
|
interface ConnectivityResult {
|
||||||
@@ -32,6 +33,228 @@ interface ProviderListProps {
|
|||||||
agent: Agent;
|
agent: Agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProviderCardProps {
|
||||||
|
provider: Provider;
|
||||||
|
agent: Agent;
|
||||||
|
defaultModel: string | null;
|
||||||
|
models: Record<string, Model>;
|
||||||
|
/** Inline connectivity-test state for this provider, or undefined when idle. */
|
||||||
|
ts: TestState | undefined;
|
||||||
|
onEdit: (name: string) => void;
|
||||||
|
onDelete: (name: string) => void;
|
||||||
|
onDuplicate: (name: string) => void;
|
||||||
|
onSwitchProvider: (name: string) => void;
|
||||||
|
onConfigureUsage: (name: string) => void;
|
||||||
|
onTest: (provider: Provider) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProviderCard({
|
||||||
|
provider,
|
||||||
|
agent,
|
||||||
|
defaultModel,
|
||||||
|
models,
|
||||||
|
ts,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onDuplicate,
|
||||||
|
onSwitchProvider,
|
||||||
|
onConfigureUsage,
|
||||||
|
onTest,
|
||||||
|
}: ProviderCardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
// Usage query state lives once per card; both the compact (card header) and
|
||||||
|
// detail (footer) UsageFooter variants read this same state, so a refresh
|
||||||
|
// from either side updates both.
|
||||||
|
const usage = useUsageQuery(
|
||||||
|
agent,
|
||||||
|
provider.name,
|
||||||
|
provider.usageKinds,
|
||||||
|
provider.usageConfig?.autoQueryIntervalMinutes
|
||||||
|
);
|
||||||
|
const providerModels = Object.values(models).filter(
|
||||||
|
(m) => m.provider === provider.name
|
||||||
|
);
|
||||||
|
const defaultModelName = defaultModel
|
||||||
|
? models[defaultModel]?.model || models[defaultModel]?.display_name || defaultModel
|
||||||
|
: null;
|
||||||
|
const isActive = provider.active === true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`group relative flex flex-col gap-3 p-4 rounded-xl border transition-colors cursor-pointer w-full ${
|
||||||
|
isActive
|
||||||
|
? "bg-green-50 dark:bg-green-900/10 border-green-300 dark:border-green-500/30 hover:border-green-400 dark:border-green-500/50 hover:bg-green-100 dark:bg-green-900/20"
|
||||||
|
: "bg-panel border-border hover:border-strong hover:bg-hover"
|
||||||
|
}`}
|
||||||
|
onClick={() => onEdit(provider.name)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 w-full">
|
||||||
|
<ProviderIcon
|
||||||
|
name={provider.name}
|
||||||
|
icon={provider.icon}
|
||||||
|
color={provider.icon_color}
|
||||||
|
size={44}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-content-primary truncate">
|
||||||
|
{provider.name}
|
||||||
|
</h3>
|
||||||
|
{isActive && (
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 border border-green-300 dark:border-green-500/30">
|
||||||
|
{t("inUse")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{provider.note && (
|
||||||
|
<span className="text-xs text-content-muted truncate max-w-[200px]">
|
||||||
|
{provider.note}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex items-center gap-3 text-sm text-content-muted flex-wrap">
|
||||||
|
<span className="font-mono text-xs">
|
||||||
|
{provider.official_url || provider.base_url || t("noUrl")}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded-full bg-hover-2 text-content-muted border border-border">
|
||||||
|
{provider.provider_type}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs">
|
||||||
|
{t("modelCount", { count: providerModels.length })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center flex-wrap justify-end gap-2">
|
||||||
|
{defaultModel &&
|
||||||
|
models[defaultModel]?.provider === provider.name && (
|
||||||
|
<span className="text-xs px-2 py-1 rounded-full bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 border border-green-300 dark:border-green-500/20">
|
||||||
|
{t("defaultModel", { name: defaultModelName ?? "" })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{/* inline connectivity test result */}
|
||||||
|
{ts && ts.status !== "testing" && (
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-0.5 rounded-full border tabular-nums ${
|
||||||
|
ts.status === "ok"
|
||||||
|
? ts.latency && ts.latency > 6000
|
||||||
|
? "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 border-orange-300 dark:border-orange-500/30"
|
||||||
|
: "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 border-green-300 dark:border-green-500/30"
|
||||||
|
: "bg-red-100 dark:bg-red-900/30 text-red-500 dark:text-red-400 border-red-300 dark:border-red-500/30"
|
||||||
|
}`}
|
||||||
|
title={
|
||||||
|
ts.status === "fail"
|
||||||
|
? ts.error || t("connectivityFail", { name: provider.name, error: "" })
|
||||||
|
: ts.latency && ts.latency > 6000
|
||||||
|
? t("connectivitySlow", { name: provider.name, latency: ts.latency ?? 0 })
|
||||||
|
: t("connectivityOk", { name: provider.name, latency: ts.latency ?? 0 })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ts.status === "ok" ? `${ts.latency}ms` : "✕"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{/* compact usage summary — sits left of the switch button,
|
||||||
|
mirroring cc-switch's card layout (usage → action buttons) */}
|
||||||
|
{(provider.usageKinds?.length ?? 0) > 0 && (
|
||||||
|
<UsageFooter usage={usage} variant="compact" />
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSwitchProvider(provider.name);
|
||||||
|
}}
|
||||||
|
className={`px-3 py-1.5 text-sm rounded focus:ring-2 focus:outline-none ${
|
||||||
|
isActive
|
||||||
|
? "bg-green-600 hover:bg-green-700 text-white focus:ring-green-500"
|
||||||
|
: "bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isActive ? t("inUse") : t("switchTo")}
|
||||||
|
</button>
|
||||||
|
{agent === "kimi_code" && (
|
||||||
|
<span
|
||||||
|
className="text-sm text-content-muted hover:text-content-primary cursor-help select-none"
|
||||||
|
title={t("switchReloadHint")}
|
||||||
|
aria-label={t("switchReloadHint")}
|
||||||
|
>
|
||||||
|
ⓘ
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* icon button group */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEdit(provider.name);
|
||||||
|
}}
|
||||||
|
title={t("edit")}
|
||||||
|
className={iconBtn}
|
||||||
|
aria-label={t("edit")}
|
||||||
|
>
|
||||||
|
<Pencil className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicate(provider.name);
|
||||||
|
}}
|
||||||
|
title={t("copyProvider")}
|
||||||
|
className={iconBtn}
|
||||||
|
aria-label={t("copyProvider")}
|
||||||
|
>
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTest(provider);
|
||||||
|
}}
|
||||||
|
disabled={ts?.status === "testing"}
|
||||||
|
title={t("testConnectivity")}
|
||||||
|
className={`${iconBtn} disabled:opacity-50`}
|
||||||
|
aria-label={t("testConnectivity")}
|
||||||
|
>
|
||||||
|
{ts?.status === "testing" ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Activity className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onConfigureUsage(provider.name);
|
||||||
|
}}
|
||||||
|
title={t("usageConfigBtn")}
|
||||||
|
className={iconBtn}
|
||||||
|
aria-label={t("usageConfigBtn")}
|
||||||
|
>
|
||||||
|
<BarChart3 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete(provider.name);
|
||||||
|
}}
|
||||||
|
title={t("delete")}
|
||||||
|
className={`${iconBtn} hover:text-red-400 hover:border-red-500/30 hover:bg-red-900/20`}
|
||||||
|
aria-label={t("delete")}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UsageFooter usage={usage} variant="detail" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const iconBtn =
|
const iconBtn =
|
||||||
"w-8 h-8 flex items-center justify-center rounded border border-border text-content-muted hover:text-content-primary hover:bg-hover-2 focus:ring-2 focus:ring-blue-500 focus:outline-none transition-colors";
|
"w-8 h-8 flex items-center justify-center rounded border border-border text-content-muted hover:text-content-primary hover:bg-hover-2 focus:ring-2 focus:ring-blue-500 focus:outline-none transition-colors";
|
||||||
|
|
||||||
@@ -139,205 +362,22 @@ export function ProviderList({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-3 w-full">
|
<div className="grid grid-cols-1 gap-3 w-full">
|
||||||
{sortedProviders.map((provider) => {
|
{sortedProviders.map((provider) => (
|
||||||
const providerModels = Object.values(models).filter(
|
<ProviderCard
|
||||||
(m) => m.provider === provider.name
|
key={provider.name}
|
||||||
);
|
provider={provider}
|
||||||
const defaultModelName = defaultModel
|
agent={agent}
|
||||||
? models[defaultModel]?.model || models[defaultModel]?.display_name || defaultModel
|
defaultModel={defaultModel}
|
||||||
: null;
|
models={models}
|
||||||
const isActive = provider.active === true;
|
ts={testState[provider.name]}
|
||||||
const ts = testState[provider.name];
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
return (
|
onDuplicate={onDuplicate}
|
||||||
<div
|
onSwitchProvider={onSwitchProvider}
|
||||||
key={provider.name}
|
onConfigureUsage={onConfigureUsage}
|
||||||
className={`group relative flex flex-col gap-3 p-4 rounded-xl border transition-colors cursor-pointer w-full ${
|
onTest={handleTest}
|
||||||
isActive
|
/>
|
||||||
? "bg-green-50 dark:bg-green-900/10 border-green-300 dark:border-green-500/30 hover:border-green-400 dark:border-green-500/50 hover:bg-green-100 dark:bg-green-900/20"
|
))}
|
||||||
: "bg-panel border-border hover:border-strong hover:bg-hover"
|
|
||||||
}`}
|
|
||||||
onClick={() => onEdit(provider.name)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4 w-full">
|
|
||||||
<ProviderIcon
|
|
||||||
name={provider.name}
|
|
||||||
icon={provider.icon}
|
|
||||||
color={provider.icon_color}
|
|
||||||
size={44}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-content-primary truncate">
|
|
||||||
{provider.name}
|
|
||||||
</h3>
|
|
||||||
{isActive && (
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 border border-green-300 dark:border-green-500/30">
|
|
||||||
{t("inUse")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{provider.note && (
|
|
||||||
<span className="text-xs text-content-muted truncate max-w-[200px]">
|
|
||||||
{provider.note}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 flex items-center gap-3 text-sm text-content-muted flex-wrap">
|
|
||||||
<span className="font-mono text-xs">
|
|
||||||
{provider.official_url || provider.base_url || t("noUrl")}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-hover-2 text-content-muted border border-border">
|
|
||||||
{provider.provider_type}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">
|
|
||||||
{t("modelCount", { count: providerModels.length })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center flex-wrap justify-end gap-2">
|
|
||||||
{defaultModel &&
|
|
||||||
models[defaultModel]?.provider === provider.name && (
|
|
||||||
<span className="text-xs px-2 py-1 rounded-full bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 border border-green-300 dark:border-green-500/20">
|
|
||||||
{t("defaultModel", { name: defaultModelName ?? "" })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{/* inline connectivity test result */}
|
|
||||||
{ts && ts.status !== "testing" && (
|
|
||||||
<span
|
|
||||||
className={`text-xs px-2 py-0.5 rounded-full border tabular-nums ${
|
|
||||||
ts.status === "ok"
|
|
||||||
? ts.latency && ts.latency > 6000
|
|
||||||
? "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 border-orange-300 dark:border-orange-500/30"
|
|
||||||
: "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 border-green-300 dark:border-green-500/30"
|
|
||||||
: "bg-red-100 dark:bg-red-900/30 text-red-500 dark:text-red-400 border-red-300 dark:border-red-500/30"
|
|
||||||
}`}
|
|
||||||
title={
|
|
||||||
ts.status === "fail"
|
|
||||||
? ts.error || t("connectivityFail", { name: provider.name, error: "" })
|
|
||||||
: ts.latency && ts.latency > 6000
|
|
||||||
? t("connectivitySlow", { name: provider.name, latency: ts.latency ?? 0 })
|
|
||||||
: t("connectivityOk", { name: provider.name, latency: ts.latency ?? 0 })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{ts.status === "ok" ? `${ts.latency}ms` : "✕"}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{/* compact usage summary — sits left of the switch button,
|
|
||||||
mirroring cc-switch's card layout (usage → action buttons) */}
|
|
||||||
{(provider.usageKinds?.length ?? 0) > 0 && (
|
|
||||||
<UsageFooter
|
|
||||||
agent={agent}
|
|
||||||
providerName={provider.name}
|
|
||||||
usageKinds={provider.usageKinds}
|
|
||||||
variant="compact"
|
|
||||||
autoIntervalMinutes={
|
|
||||||
provider.usageConfig?.autoQueryIntervalMinutes
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onSwitchProvider(provider.name);
|
|
||||||
}}
|
|
||||||
className={`px-3 py-1.5 text-sm rounded focus:ring-2 focus:outline-none ${
|
|
||||||
isActive
|
|
||||||
? "bg-green-600 hover:bg-green-700 text-white focus:ring-green-500"
|
|
||||||
: "bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isActive ? t("inUse") : t("switchTo")}
|
|
||||||
</button>
|
|
||||||
{agent === "kimi_code" && (
|
|
||||||
<span
|
|
||||||
className="text-sm text-content-muted hover:text-content-primary cursor-help select-none"
|
|
||||||
title={t("switchReloadHint")}
|
|
||||||
aria-label={t("switchReloadHint")}
|
|
||||||
>
|
|
||||||
ⓘ
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* icon button group */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onEdit(provider.name);
|
|
||||||
}}
|
|
||||||
title={t("edit")}
|
|
||||||
className={iconBtn}
|
|
||||||
aria-label={t("edit")}
|
|
||||||
>
|
|
||||||
<Pencil className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDuplicate(provider.name);
|
|
||||||
}}
|
|
||||||
title={t("copyProvider")}
|
|
||||||
className={iconBtn}
|
|
||||||
aria-label={t("copyProvider")}
|
|
||||||
>
|
|
||||||
<Copy className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleTest(provider);
|
|
||||||
}}
|
|
||||||
disabled={ts?.status === "testing"}
|
|
||||||
title={t("testConnectivity")}
|
|
||||||
className={`${iconBtn} disabled:opacity-50`}
|
|
||||||
aria-label={t("testConnectivity")}
|
|
||||||
>
|
|
||||||
{ts?.status === "testing" ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Activity className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onConfigureUsage(provider.name);
|
|
||||||
}}
|
|
||||||
title={t("usageConfigBtn")}
|
|
||||||
className={iconBtn}
|
|
||||||
aria-label={t("usageConfigBtn")}
|
|
||||||
>
|
|
||||||
<BarChart3 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDelete(provider.name);
|
|
||||||
}}
|
|
||||||
title={t("delete")}
|
|
||||||
className={`${iconBtn} hover:text-red-400 hover:border-red-500/30 hover:bg-red-900/20`}
|
|
||||||
aria-label={t("delete")}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<UsageFooter
|
|
||||||
agent={agent}
|
|
||||||
providerName={provider.name}
|
|
||||||
usageKinds={provider.usageKinds}
|
|
||||||
variant="detail"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,11 +44,17 @@ const EFFORT_LABELS: Record<(typeof EFFORTS)[number], TranslationKey> = {
|
|||||||
const FLAG_LABELS: Record<string, { name: TranslationKey; desc: TranslationKey }> = {
|
const FLAG_LABELS: Record<string, { name: TranslationKey; desc: TranslationKey }> = {
|
||||||
"secondary-model": { name: "flagSecondaryModel", desc: "flagSecondaryModelDesc" },
|
"secondary-model": { name: "flagSecondaryModel", desc: "flagSecondaryModelDesc" },
|
||||||
"tool-select": { name: "flagToolSelect", desc: "flagToolSelectDesc" },
|
"tool-select": { name: "flagToolSelect", desc: "flagToolSelectDesc" },
|
||||||
"acp-v2": { name: "flagAcpV2", desc: "flagAcpV2Desc" },
|
|
||||||
persistence_minidb_readmodel: {
|
persistence_minidb_readmodel: {
|
||||||
name: "flagMinidbReadmodel",
|
name: "flagMinidbReadmodel",
|
||||||
desc: "flagMinidbReadmodelDesc",
|
desc: "flagMinidbReadmodelDesc",
|
||||||
},
|
},
|
||||||
|
tower: { name: "flagTower", desc: "flagTowerDesc" },
|
||||||
|
subagent_fork: { name: "flagSubagentFork", desc: "flagSubagentForkDesc" },
|
||||||
|
wait_for: { name: "flagWaitFor", desc: "flagWaitForDesc" },
|
||||||
|
auto_session_title: {
|
||||||
|
name: "flagAutoSessionTitle",
|
||||||
|
desc: "flagAutoSessionTitleDesc",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 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. */
|
||||||
|
|||||||
+10
-162
@@ -1,175 +1,23 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { Loader2, RefreshCw } from "lucide-react";
|
import { Loader2, RefreshCw } from "lucide-react";
|
||||||
import { useTranslation } from "../i18n";
|
import { useTranslation } from "../i18n";
|
||||||
import { localizeUsageError, planLabel } from "../lib/usage-display";
|
import { localizeUsageError, planLabel } from "../lib/usage-display";
|
||||||
import type { Agent } from "../types";
|
import type { UsageData, UsageQueryState } from "../hooks/useUsageQuery";
|
||||||
|
|
||||||
interface UsageData {
|
|
||||||
planName?: string | null;
|
|
||||||
remaining?: number | null;
|
|
||||||
total?: number | null;
|
|
||||||
used?: number | null;
|
|
||||||
unit?: string | null;
|
|
||||||
isValid?: boolean | null;
|
|
||||||
resetsAt?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UsageResult {
|
|
||||||
success: boolean;
|
|
||||||
data?: UsageData[] | null;
|
|
||||||
error?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
type UsageStatus = "idle" | "loading" | "success" | "error";
|
|
||||||
|
|
||||||
interface CacheEntry {
|
|
||||||
status: "success" | "error";
|
|
||||||
data: UsageData[];
|
|
||||||
/** Raw error text from Rust; null = transient failure (invoke rejected). */
|
|
||||||
error: string | null;
|
|
||||||
updatedAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module-level cache shared across mounts: re-entering the list within the
|
|
||||||
// stale TTL shows the last result without firing new requests. Both the
|
|
||||||
// compact (card header) and detail (footer) variants read this same cache.
|
|
||||||
const STALE_TTL_MS = 5 * 60 * 1000;
|
|
||||||
const cache = new Map<string, CacheEntry>();
|
|
||||||
|
|
||||||
// Simple semaphore: at most MAX_CONCURRENT queries in flight at once.
|
|
||||||
const MAX_CONCURRENT = 3;
|
|
||||||
let running = 0;
|
|
||||||
const waiters: Array<() => void> = [];
|
|
||||||
async function acquireSlot(): Promise<void> {
|
|
||||||
if (running >= MAX_CONCURRENT) {
|
|
||||||
await new Promise<void>((resolve) => waiters.push(resolve));
|
|
||||||
}
|
|
||||||
running += 1;
|
|
||||||
}
|
|
||||||
function releaseSlot(): void {
|
|
||||||
running -= 1;
|
|
||||||
waiters.shift()?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UsageFooterProps {
|
interface UsageFooterProps {
|
||||||
agent: Agent;
|
/** Query state lifted to the provider card via useUsageQuery. Both the
|
||||||
providerName: string;
|
* compact (card header) and detail (footer) variants read the same state,
|
||||||
usageKinds?: string[];
|
* so a refresh from either updates both. */
|
||||||
|
usage: UsageQueryState;
|
||||||
/** "detail" (default) renders the multi-line footer; "compact" renders a
|
/** "detail" (default) renders the multi-line footer; "compact" renders a
|
||||||
* one-line summary + last-updated + refresh button for the card header. */
|
* one-line summary + last-updated + refresh button for the card header. */
|
||||||
variant?: "detail" | "compact";
|
variant?: "detail" | "compact";
|
||||||
/** Auto query interval in minutes; only wired on the compact variant so
|
|
||||||
* both variants do not double-fire. 0/undefined = manual only. */
|
|
||||||
autoIntervalMinutes?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UsageFooter({
|
export function UsageFooter({ usage, variant = "detail" }: UsageFooterProps) {
|
||||||
agent,
|
|
||||||
providerName,
|
|
||||||
usageKinds,
|
|
||||||
variant = "detail",
|
|
||||||
autoIntervalMinutes,
|
|
||||||
}: UsageFooterProps) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const supported = (usageKinds?.length ?? 0) > 0;
|
const { status, data, error, updatedAt, supported, refresh } = usage;
|
||||||
// Cache key includes the agent so a Kimi Code provider and a Pi provider
|
|
||||||
// with the same name do not clobber each other's cached result.
|
|
||||||
const cacheKey = `${agent}:${providerName}`;
|
|
||||||
|
|
||||||
const [status, setStatus] = useState<UsageStatus>("idle");
|
|
||||||
const [data, setData] = useState<UsageData[]>([]);
|
|
||||||
/** undefined = no error; null = network error; string = Rust error text. */
|
|
||||||
const [error, setError] = useState<string | null | undefined>(undefined);
|
|
||||||
const [updatedAt, setUpdatedAt] = useState<number | null>(null);
|
|
||||||
const [now, setNow] = useState(() => Date.now());
|
const [now, setNow] = useState(() => Date.now());
|
||||||
// Generation counter: stale responses (unmounted / superseded query) are ignored.
|
|
||||||
const genRef = useRef(0);
|
|
||||||
|
|
||||||
const runQuery = useCallback(
|
|
||||||
async (forceRefresh: boolean) => {
|
|
||||||
const gen = ++genRef.current;
|
|
||||||
setStatus("loading");
|
|
||||||
setError(undefined);
|
|
||||||
const finish = (entry: CacheEntry) => {
|
|
||||||
cache.set(cacheKey, entry);
|
|
||||||
if (genRef.current !== gen) return;
|
|
||||||
setStatus(entry.status);
|
|
||||||
setData(entry.data);
|
|
||||||
setError(entry.error);
|
|
||||||
setUpdatedAt(entry.updatedAt);
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await acquireSlot();
|
|
||||||
let result: UsageResult;
|
|
||||||
try {
|
|
||||||
result = await invoke<UsageResult>("query_provider_usage", {
|
|
||||||
agent,
|
|
||||||
providerName,
|
|
||||||
forceRefresh,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
releaseSlot();
|
|
||||||
}
|
|
||||||
if (genRef.current !== gen) return;
|
|
||||||
if (result.success) {
|
|
||||||
finish({
|
|
||||||
status: "success",
|
|
||||||
data: result.data ?? [],
|
|
||||||
error: null,
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Deterministic failure: keep last good data for ghost display.
|
|
||||||
finish({
|
|
||||||
status: "error",
|
|
||||||
data: cache.get(cacheKey)?.data ?? [],
|
|
||||||
error: result.error ?? "",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Transient failure (network / timeout): invoke rejected.
|
|
||||||
finish({
|
|
||||||
status: "error",
|
|
||||||
data: cache.get(cacheKey)?.data ?? [],
|
|
||||||
error: null,
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[agent, providerName, cacheKey]
|
|
||||||
);
|
|
||||||
|
|
||||||
// On mount / provider change: serve fresh cache, otherwise query once.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!supported) return;
|
|
||||||
const cached = cache.get(cacheKey);
|
|
||||||
if (cached) {
|
|
||||||
setStatus(cached.status);
|
|
||||||
setData(cached.data);
|
|
||||||
setError(cached.error);
|
|
||||||
setUpdatedAt(cached.updatedAt);
|
|
||||||
if (Date.now() - cached.updatedAt < STALE_TTL_MS) return;
|
|
||||||
}
|
|
||||||
void runQuery(false);
|
|
||||||
}, [supported, cacheKey, runQuery]);
|
|
||||||
|
|
||||||
// Ignore late responses after unmount.
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
genRef.current += 1;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Auto query interval (compact variant only, to avoid double-firing).
|
|
||||||
useEffect(() => {
|
|
||||||
if (variant !== "compact" || !supported) return;
|
|
||||||
const mins = autoIntervalMinutes ?? 0;
|
|
||||||
if (mins <= 0) return;
|
|
||||||
const id = setInterval(() => void runQuery(false), mins * 60_000);
|
|
||||||
return () => clearInterval(id);
|
|
||||||
}, [variant, supported, autoIntervalMinutes, runQuery]);
|
|
||||||
|
|
||||||
// Tick relative times once a minute while showing data.
|
// Tick relative times once a minute while showing data.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -226,7 +74,7 @@ export function UsageFooter({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void runQuery(true);
|
void refresh(true);
|
||||||
}}
|
}}
|
||||||
disabled={status === "loading"}
|
disabled={status === "loading"}
|
||||||
title={t("usageRefresh")}
|
title={t("usageRefresh")}
|
||||||
@@ -344,7 +192,7 @@ export function UsageFooter({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void runQuery(true);
|
void refresh(true);
|
||||||
}}
|
}}
|
||||||
className="ml-auto shrink-0 px-2 py-0.5 rounded border border-red-300 dark:border-red-500/30 text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
className="ml-auto shrink-0 px-2 py-0.5 rounded border border-red-300 dark:border-red-500/30 text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { Agent } from "../types";
|
||||||
|
|
||||||
|
export interface UsageData {
|
||||||
|
planName?: string | null;
|
||||||
|
remaining?: number | null;
|
||||||
|
total?: number | null;
|
||||||
|
used?: number | null;
|
||||||
|
unit?: string | null;
|
||||||
|
isValid?: boolean | null;
|
||||||
|
resetsAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsageResult {
|
||||||
|
success: boolean;
|
||||||
|
data?: UsageData[] | null;
|
||||||
|
error?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UsageStatus = "idle" | "loading" | "success" | "error";
|
||||||
|
|
||||||
|
export interface CacheEntry {
|
||||||
|
status: "success" | "error";
|
||||||
|
data: UsageData[];
|
||||||
|
/** Raw error text from Rust; null = transient failure (invoke rejected). */
|
||||||
|
error: string | null;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module-level cache shared across mounts: re-entering the list within the
|
||||||
|
// stale TTL shows the last result without firing new requests. Both the
|
||||||
|
// compact (card header) and detail (footer) variants read this same cache.
|
||||||
|
const STALE_TTL_MS = 5 * 60 * 1000;
|
||||||
|
const cache = new Map<string, CacheEntry>();
|
||||||
|
|
||||||
|
// Simple semaphore: at most MAX_CONCURRENT queries in flight at once.
|
||||||
|
const MAX_CONCURRENT = 3;
|
||||||
|
let running = 0;
|
||||||
|
const waiters: Array<() => void> = [];
|
||||||
|
async function acquireSlot(): Promise<void> {
|
||||||
|
if (running >= MAX_CONCURRENT) {
|
||||||
|
await new Promise<void>((resolve) => waiters.push(resolve));
|
||||||
|
}
|
||||||
|
running += 1;
|
||||||
|
}
|
||||||
|
function releaseSlot(): void {
|
||||||
|
running -= 1;
|
||||||
|
waiters.shift()?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsageQueryState {
|
||||||
|
status: UsageStatus;
|
||||||
|
data: UsageData[];
|
||||||
|
error: string | null | undefined;
|
||||||
|
updatedAt: number | null;
|
||||||
|
refresh: (force: boolean) => void;
|
||||||
|
supported: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUsageQuery(
|
||||||
|
agent: Agent,
|
||||||
|
providerName: string,
|
||||||
|
usageKinds?: string[],
|
||||||
|
autoIntervalMinutes?: number
|
||||||
|
): UsageQueryState {
|
||||||
|
const supported = (usageKinds?.length ?? 0) > 0;
|
||||||
|
// Cache key includes the agent so a Kimi Code provider and a Pi provider
|
||||||
|
// with the same name do not clobber each other's cached result.
|
||||||
|
const cacheKey = `${agent}:${providerName}`;
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<UsageStatus>("idle");
|
||||||
|
const [data, setData] = useState<UsageData[]>([]);
|
||||||
|
/** undefined = no error; null = network error; string = Rust error text. */
|
||||||
|
const [error, setError] = useState<string | null | undefined>(undefined);
|
||||||
|
const [updatedAt, setUpdatedAt] = useState<number | null>(null);
|
||||||
|
// Generation counter: stale responses (unmounted / superseded query) are ignored.
|
||||||
|
const genRef = useRef(0);
|
||||||
|
|
||||||
|
const runQuery = useCallback(
|
||||||
|
async (forceRefresh: boolean) => {
|
||||||
|
const gen = ++genRef.current;
|
||||||
|
setStatus("loading");
|
||||||
|
setError(undefined);
|
||||||
|
const finish = (entry: CacheEntry) => {
|
||||||
|
cache.set(cacheKey, entry);
|
||||||
|
if (genRef.current !== gen) return;
|
||||||
|
setStatus(entry.status);
|
||||||
|
setData(entry.data);
|
||||||
|
setError(entry.error);
|
||||||
|
setUpdatedAt(entry.updatedAt);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await acquireSlot();
|
||||||
|
let result: UsageResult;
|
||||||
|
try {
|
||||||
|
result = await invoke<UsageResult>("query_provider_usage", {
|
||||||
|
agent,
|
||||||
|
providerName,
|
||||||
|
forceRefresh,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
releaseSlot();
|
||||||
|
}
|
||||||
|
if (genRef.current !== gen) return;
|
||||||
|
if (result.success) {
|
||||||
|
finish({
|
||||||
|
status: "success",
|
||||||
|
data: result.data ?? [],
|
||||||
|
error: null,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Deterministic failure: keep last good data for ghost display.
|
||||||
|
finish({
|
||||||
|
status: "error",
|
||||||
|
data: cache.get(cacheKey)?.data ?? [],
|
||||||
|
error: result.error ?? "",
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Transient failure (network / timeout): invoke rejected.
|
||||||
|
finish({
|
||||||
|
status: "error",
|
||||||
|
data: cache.get(cacheKey)?.data ?? [],
|
||||||
|
error: null,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[agent, providerName, cacheKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
// On mount / provider change: serve fresh cache, otherwise query once.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!supported) return;
|
||||||
|
const cached = cache.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
setStatus(cached.status);
|
||||||
|
setData(cached.data);
|
||||||
|
setError(cached.error);
|
||||||
|
setUpdatedAt(cached.updatedAt);
|
||||||
|
if (Date.now() - cached.updatedAt < STALE_TTL_MS) return;
|
||||||
|
}
|
||||||
|
void runQuery(false);
|
||||||
|
}, [supported, cacheKey, runQuery]);
|
||||||
|
|
||||||
|
// Ignore late responses after unmount.
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
genRef.current += 1;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auto query interval — the hook is called once per provider card and both
|
||||||
|
// variants read the same state, so there is no second mount to double-fire.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!supported) return;
|
||||||
|
const mins = autoIntervalMinutes ?? 0;
|
||||||
|
if (mins <= 0) return;
|
||||||
|
const id = setInterval(() => void runQuery(false), mins * 60_000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [supported, autoIntervalMinutes, runQuery]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
updatedAt,
|
||||||
|
refresh: runQuery,
|
||||||
|
supported,
|
||||||
|
};
|
||||||
|
}
|
||||||
+8
-2
@@ -465,10 +465,16 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
flagSecondaryModelDesc: "Newly spawned subagents bind a second (usually cheaper) model config",
|
flagSecondaryModelDesc: "Newly spawned subagents bind a second (usually cheaper) model config",
|
||||||
flagToolSelect: "Tool Select",
|
flagToolSelect: "Tool Select",
|
||||||
flagToolSelectDesc: "Enable the experimental tool selection feature",
|
flagToolSelectDesc: "Enable the experimental tool selection feature",
|
||||||
flagAcpV2: "ACP v2 Protocol",
|
|
||||||
flagAcpV2Desc: "Enable Agent Communication Protocol v2",
|
|
||||||
flagMinidbReadmodel: "MiniDB Read Model",
|
flagMinidbReadmodel: "MiniDB Read Model",
|
||||||
flagMinidbReadmodelDesc: "Enable the v2 engine MiniDB read-model backend",
|
flagMinidbReadmodelDesc: "Enable the v2 engine MiniDB read-model backend",
|
||||||
|
flagTower: "Tower Mode",
|
||||||
|
flagTowerDesc: "Enable tower mode: coordinate multiple agents on a shared objective (/tower command)",
|
||||||
|
flagSubagentFork: "Subagent Fork Context",
|
||||||
|
flagSubagentForkDesc: "Agent/AgentSwarm tools can start a subagent with a snapshot of the calling agent's history (fork parameter)",
|
||||||
|
flagWaitFor: "WaitFor Tool",
|
||||||
|
flagWaitForDesc: "The agent can wait for background tasks to finish within the current turn",
|
||||||
|
flagAutoSessionTitle: "Auto Session Title",
|
||||||
|
flagAutoSessionTitleDesc: "Generate session titles automatically via the managed chat_title tool",
|
||||||
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: all experimental features are forced on by the env var",
|
||||||
secondaryModelSection: "Subagent Model (Secondary Model)",
|
secondaryModelSection: "Subagent Model (Secondary Model)",
|
||||||
|
|||||||
+8
-2
@@ -460,10 +460,16 @@ export const zhTranslations = {
|
|||||||
flagSecondaryModelDesc: "子代理派发时绑定第二份(通常更便宜的)模型配置",
|
flagSecondaryModelDesc: "子代理派发时绑定第二份(通常更便宜的)模型配置",
|
||||||
flagToolSelect: "工具选择",
|
flagToolSelect: "工具选择",
|
||||||
flagToolSelectDesc: "启用实验性工具选择功能",
|
flagToolSelectDesc: "启用实验性工具选择功能",
|
||||||
flagAcpV2: "ACP v2 协议",
|
|
||||||
flagAcpV2Desc: "启用 Agent Communication Protocol v2",
|
|
||||||
flagMinidbReadmodel: "MiniDB 读模型",
|
flagMinidbReadmodel: "MiniDB 读模型",
|
||||||
flagMinidbReadmodelDesc: "启用 v2 引擎 MiniDB 读模型后端",
|
flagMinidbReadmodelDesc: "启用 v2 引擎 MiniDB 读模型后端",
|
||||||
|
flagTower: "Tower 模式",
|
||||||
|
flagTowerDesc: "启用 Tower 多智能体协同模式(/tower 命令切换)",
|
||||||
|
flagSubagentFork: "子代理 Fork 上下文",
|
||||||
|
flagSubagentForkDesc: "Agent/AgentSwarm 工具可携带主代理会话快照启动子代理(fork 参数)",
|
||||||
|
flagWaitFor: "WaitFor 工具",
|
||||||
|
flagWaitForDesc: "代理可在当前回合内等待后台任务完成,而无需结束回合再被唤醒",
|
||||||
|
flagAutoSessionTitle: "自动会话标题",
|
||||||
|
flagAutoSessionTitleDesc: "通过托管 chat_title 工具自动生成会话标题",
|
||||||
lockedByEnv: "被环境变量锁定",
|
lockedByEnv: "被环境变量锁定",
|
||||||
masterEnvOnHint: "KIMI_CODE_EXPERIMENTAL_FLAG 已设置:全部实验功能被环境变量强制开启",
|
masterEnvOnHint: "KIMI_CODE_EXPERIMENTAL_FLAG 已设置:全部实验功能被环境变量强制开启",
|
||||||
secondaryModelSection: "子代理模型(次主力模型)",
|
secondaryModelSection: "子代理模型(次主力模型)",
|
||||||
|
|||||||
+1793
-1071
File diff suppressed because it is too large.
Load diff
+1510
-910
File diff suppressed because it is too large.
Load diff
@@ -30,15 +30,25 @@ export interface ExperimentalFlagDef {
|
|||||||
envVar: string;
|
envVar: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Known experimental flags (Kimi Code v1 + v2 registries, merged). */
|
/** Known experimental flags — mirrors the kimi-code v2 flag registry
|
||||||
|
* (the per-feature flag.ts files under packages/agent-core-v2/src:
|
||||||
|
* secondary-model, tool-select, persistence_minidb_readmodel, tower,
|
||||||
|
* subagent_fork, wait_for, auto_session_title). `acp-v2` was removed
|
||||||
|
* upstream and is dropped here. */
|
||||||
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" },
|
||||||
{ id: "tool-select", envVar: "KIMI_CODE_EXPERIMENTAL_TOOL_SELECT" },
|
{ id: "tool-select", envVar: "KIMI_CODE_EXPERIMENTAL_TOOL_SELECT" },
|
||||||
{ id: "acp-v2", envVar: "KIMI_CODE_EXPERIMENTAL_ACP_V2" },
|
|
||||||
{
|
{
|
||||||
id: "persistence_minidb_readmodel",
|
id: "persistence_minidb_readmodel",
|
||||||
envVar: "KIMI_CODE_EXPERIMENTAL_PERSISTENCE_MINIDB_READMODEL",
|
envVar: "KIMI_CODE_EXPERIMENTAL_PERSISTENCE_MINIDB_READMODEL",
|
||||||
},
|
},
|
||||||
|
{ id: "tower", envVar: "KIMI_CODE_EXPERIMENTAL_TOWER" },
|
||||||
|
{ id: "subagent_fork", envVar: "KIMI_CODE_EXPERIMENTAL_SUBAGENT_FORK" },
|
||||||
|
{ id: "wait_for", envVar: "KIMI_CODE_EXPERIMENTAL_WAIT_FOR" },
|
||||||
|
{
|
||||||
|
id: "auto_session_title",
|
||||||
|
envVar: "KIMI_CODE_EXPERIMENTAL_AUTO_SESSION_TITLE",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EXPERIMENTAL_MASTER_ENV = "KIMI_CODE_EXPERIMENTAL_FLAG";
|
export const EXPERIMENTAL_MASTER_ENV = "KIMI_CODE_EXPERIMENTAL_FLAG";
|
||||||
|
|||||||
Reference in new issue
Block a user