Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d120ad2bb | ||
|
|
211986a1e5 | ||
|
|
a394e5f730 | ||
|
|
b01a5661b4 | ||
|
|
3b12844342 | ||
|
|
675e8ba24d | ||
|
|
5fcdc333b1 | ||
|
|
2b25c62ffc | ||
|
|
0cd70b9dbc | ||
|
|
70c52aeaca |
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,192 @@
|
|||||||
|
# Kimi Code /reload Hint Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add an info icon tooltip next to the "switch provider" button, visible only in the Kimi Code tab, reminding users to run `/reload` after switching providers.
|
||||||
|
|
||||||
|
**Architecture:** Extend `ProviderList` to receive the current `agent` and conditionally render a hint icon beside the switch button. Keep changes minimal and UI-only; no state or backend changes.
|
||||||
|
|
||||||
|
**Tech Stack:** React 18, TypeScript, Tailwind CSS, custom i18n module (`src/i18n`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add i18n translation keys
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/i18n/zh.ts`
|
||||||
|
- Modify: `src/i18n/en.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add Chinese translation**
|
||||||
|
|
||||||
|
Insert `switchReloadHint` into the "Provider list" section of `src/i18n/zh.ts`, e.g. after `switchTo`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
switchReloadHint: "切换供应商后,请在 Kimi Code 中执行 /reload 以生效。",
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add English translation**
|
||||||
|
|
||||||
|
Insert the matching key into `src/i18n/en.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
switchReloadHint: "After switching providers, run /reload in Kimi Code to apply the change.",
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/i18n/zh.ts src/i18n/en.ts
|
||||||
|
git commit -m "i18n: add switchReloadHint for Kimi Code reload reminder"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Update ProviderList to accept agent and render hint
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/components/ProviderList.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Import Agent type and update props**
|
||||||
|
|
||||||
|
Add `Agent` to the import from `../types` and add `agent` to `ProviderListProps`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTranslation } from "../i18n";
|
||||||
|
import type { Agent, Model, Provider } from "../types";
|
||||||
|
|
||||||
|
interface ProviderListProps {
|
||||||
|
providers: Provider[];
|
||||||
|
defaultModel: string | null;
|
||||||
|
models: Record<string, Model>;
|
||||||
|
onEdit: (name: string) => void;
|
||||||
|
onDelete: (name: string) => void;
|
||||||
|
onAdd: () => void;
|
||||||
|
onSwitchProvider: (name: string) => void;
|
||||||
|
agent: Agent;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Destructure agent in component signature**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export function ProviderList({
|
||||||
|
providers,
|
||||||
|
defaultModel,
|
||||||
|
models,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onAdd,
|
||||||
|
onSwitchProvider,
|
||||||
|
agent,
|
||||||
|
}: ProviderListProps) {
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Render hint icon next to the switch button**
|
||||||
|
|
||||||
|
In the supplier card actions area, place an info icon immediately after the "切换使用" / "Switch to" button, rendered only when `agent === "kimi_code"`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<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-gray-500 hover:text-gray-300 cursor-help select-none"
|
||||||
|
title={t("switchReloadHint")}
|
||||||
|
aria-label={t("switchReloadHint")}
|
||||||
|
>
|
||||||
|
ⓘ
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/components/ProviderList.tsx
|
||||||
|
git commit -m "feat: show /reload hint next to switch provider button in Kimi Code tab"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Pass agent from App to ProviderList
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/App.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add agent prop to ProviderList usage**
|
||||||
|
|
||||||
|
Locate the `<ProviderList ... />` call in `src/App.tsx` and add `agent={agent}`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ProviderList
|
||||||
|
providers={providers}
|
||||||
|
defaultModel={config.default_model}
|
||||||
|
models={config.models}
|
||||||
|
onEdit={(name) => {
|
||||||
|
setEditingProvider(name);
|
||||||
|
setView("edit");
|
||||||
|
}}
|
||||||
|
onDelete={handleDeleteProvider}
|
||||||
|
onAdd={handleAddProvider}
|
||||||
|
onSwitchProvider={handleSwitchProvider}
|
||||||
|
agent={agent}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/App.tsx
|
||||||
|
git commit -m "chore: pass current agent to ProviderList"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Verify with type check and manual test
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run TypeScript compilation**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `tsc` completes without errors and Vite builds successfully.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the app**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run tauri-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Manual verification**
|
||||||
|
|
||||||
|
1. Select the **Kimi Code** tab.
|
||||||
|
2. Hover over the `ⓘ` icon next to any provider's "切换使用" / "Switch to" button.
|
||||||
|
3. Confirm the tooltip reads:
|
||||||
|
- 中文:`切换供应商后,请在 Kimi Code 中执行 /reload 以生效。`
|
||||||
|
- English: `After switching providers, run /reload in Kimi Code to apply the change.`
|
||||||
|
4. Switch to the **Pi** tab and confirm the `ⓘ` icon is **not** present.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit verification notes (optional)**
|
||||||
|
|
||||||
|
If any fixes were needed, commit them; otherwise no additional commit is required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- **Spec coverage:** Every design requirement (icon next to switch button, Kimi Code only, i18n, no new dependencies) maps to a task above.
|
||||||
|
- **Placeholder scan:** No TBD/TODO/fill-in-details present.
|
||||||
|
- **Type consistency:** `Agent` type is imported from `../types` and used consistently in props and conditional rendering.
|
||||||
@@ -0,0 +1,937 @@
|
|||||||
|
# Kimi Code 全局配置面板与模型默认上下文 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add a Kimi Code global settings panel inside ProviderEdit and auto-set `max_context_size` for new models based on a model-ID lookup table.
|
||||||
|
|
||||||
|
**Architecture:** Extend the shared `Config` type with typed agent settings, add two pure helper modules (`agent-settings.ts` for `raw_other` IO and `model-defaults.ts` for context lookup), and render the panel in a new React component wired through `ProviderEdit`.
|
||||||
|
|
||||||
|
**Tech Stack:** React 18, TypeScript, Tailwind CSS, custom i18n, Tauri v2 backend (no Rust changes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add agent settings types
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/types/index.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Append new types**
|
||||||
|
|
||||||
|
Insert at the end of `src/types/index.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface ThinkingConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
effort?: "low" | "medium" | "high" | "max";
|
||||||
|
keep?: "all" | false | 0 | "no" | "off" | "none" | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoopControlConfig {
|
||||||
|
max_retries_per_step?: number;
|
||||||
|
reserved_context_size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackgroundConfig {
|
||||||
|
max_running_tasks?: number;
|
||||||
|
keep_alive_on_exit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionRule {
|
||||||
|
decision?: "allow" | "deny" | "ask";
|
||||||
|
scope?: string;
|
||||||
|
pattern?: string;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Hook {
|
||||||
|
event?: string;
|
||||||
|
matcher?: string;
|
||||||
|
command?: string;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentSettings {
|
||||||
|
thinking?: ThinkingConfig;
|
||||||
|
loop_control?: LoopControlConfig;
|
||||||
|
background?: BackgroundConfig;
|
||||||
|
permission?: { rules?: PermissionRule[] };
|
||||||
|
hooks?: Hook[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/types/index.ts
|
||||||
|
git commit -m "types: add AgentSettings and related Kimi Code config types"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Create model default context size lookup
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/lib/model-defaults.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the lookup module**
|
||||||
|
|
||||||
|
Create `src/lib/model-defaults.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const DEFAULT_MAX_CONTEXT_SIZE = 256000;
|
||||||
|
|
||||||
|
interface ModelContextRule {
|
||||||
|
pattern: RegExp;
|
||||||
|
max_context_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODEL_CONTEXT_RULES: ModelContextRule[] = [
|
||||||
|
// Kimi
|
||||||
|
{ pattern: /^kimi-for-coding$/i, max_context_size: 262144 },
|
||||||
|
{ pattern: /^kimi-k2\.5/i, max_context_size: 256000 },
|
||||||
|
{ pattern: /^kimi-k2/i, max_context_size: 256000 },
|
||||||
|
{ pattern: /^kimi-/i, max_context_size: 256000 },
|
||||||
|
// GLM
|
||||||
|
{ pattern: /^glm-5\.2/i, max_context_size: 1000000 },
|
||||||
|
{ pattern: /^glm-5\.1/i, max_context_size: 256000 },
|
||||||
|
{ pattern: /^glm-5/i, max_context_size: 256000 },
|
||||||
|
{ pattern: /^glm-4/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^glm-/i, max_context_size: 128000 },
|
||||||
|
// MiniMax
|
||||||
|
{ pattern: /^MiniMax-M3/i, max_context_size: 1000000 },
|
||||||
|
{ pattern: /^MiniMax-Text-01/i, max_context_size: 400000 },
|
||||||
|
{ pattern: /^MiniMax-/i, max_context_size: 256000 },
|
||||||
|
// Qwen
|
||||||
|
{ pattern: /^qwen2\.5/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^qwen-max/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^qwen-plus/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^qwen-turbo/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^qwen-coder/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^qwen-/i, max_context_size: 128000 },
|
||||||
|
// DeepSeek
|
||||||
|
{ pattern: /^deepseek-r1/i, max_context_size: 64000 },
|
||||||
|
{ pattern: /^deepseek-v3/i, max_context_size: 64000 },
|
||||||
|
{ pattern: /^deepseek-coder/i, max_context_size: 64000 },
|
||||||
|
{ pattern: /^deepseek-/i, max_context_size: 64000 },
|
||||||
|
// Hunyuan
|
||||||
|
{ pattern: /^hunyuan-pro/i, max_context_size: 32000 },
|
||||||
|
{ pattern: /^hunyuan-standard/i, max_context_size: 32000 },
|
||||||
|
{ pattern: /^hunyuan-lite/i, max_context_size: 32000 },
|
||||||
|
{ pattern: /^hunyuan-/i, max_context_size: 32000 },
|
||||||
|
// Doubao
|
||||||
|
{ pattern: /^doubao-pro/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^doubao-lite/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^doubao-vision/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^doubao-/i, max_context_size: 128000 },
|
||||||
|
// ERNIE
|
||||||
|
{ pattern: /^ernie-4\.0/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^ernie-3\.5/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^ernie-speed/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^ernie-lite/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^ernie-/i, max_context_size: 128000 },
|
||||||
|
// Spark
|
||||||
|
{ pattern: /^spark-v4/i, max_context_size: 32000 },
|
||||||
|
{ pattern: /^spark-v3\.5/i, max_context_size: 32000 },
|
||||||
|
{ pattern: /^spark-pro/i, max_context_size: 32000 },
|
||||||
|
{ pattern: /^spark-max/i, max_context_size: 32000 },
|
||||||
|
{ pattern: /^spark-/i, max_context_size: 32000 },
|
||||||
|
// SenseChat
|
||||||
|
{ pattern: /^sensechat-/i, max_context_size: 128000 },
|
||||||
|
// Baichuan
|
||||||
|
{ pattern: /^baichuan-4/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^baichuan-3/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^baichuan-/i, max_context_size: 128000 },
|
||||||
|
// Yi
|
||||||
|
{ pattern: /^yi-/i, max_context_size: 128000 },
|
||||||
|
// Claude
|
||||||
|
{ pattern: /^claude-opus/i, max_context_size: 200000 },
|
||||||
|
{ pattern: /^claude-sonnet/i, max_context_size: 200000 },
|
||||||
|
{ pattern: /^claude-haiku/i, max_context_size: 200000 },
|
||||||
|
{ pattern: /^claude-/i, max_context_size: 200000 },
|
||||||
|
// OpenAI
|
||||||
|
{ pattern: /^gpt-4\.1/i, max_context_size: 1047576 },
|
||||||
|
{ pattern: /^gpt-4o/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^gpt-4-turbo/i, max_context_size: 128000 },
|
||||||
|
{ pattern: /^gpt-4-/i, max_context_size: 128000 },
|
||||||
|
// Gemini
|
||||||
|
{ pattern: /^gemini-2\.0-flash/i, max_context_size: 1048576 },
|
||||||
|
{ pattern: /^gemini-1\.5-pro/i, max_context_size: 2097152 },
|
||||||
|
{ pattern: /^gemini-1\.5-flash/i, max_context_size: 1048576 },
|
||||||
|
{ pattern: /^gemini-/i, max_context_size: 1048576 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getDefaultMaxContextSize(modelId: string): number {
|
||||||
|
for (const rule of MODEL_CONTEXT_RULES) {
|
||||||
|
if (rule.pattern.test(modelId)) {
|
||||||
|
return rule.max_context_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DEFAULT_MAX_CONTEXT_SIZE;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/lib/model-defaults.ts
|
||||||
|
git commit -m "feat: add model default max_context_size lookup table"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Create agent settings IO helpers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/lib/agent-settings.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create helper module**
|
||||||
|
|
||||||
|
Create `src/lib/agent-settings.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { AgentSettings } from "../types";
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: AgentSettings = {
|
||||||
|
thinking: {
|
||||||
|
enabled: true,
|
||||||
|
effort: "medium",
|
||||||
|
keep: "all",
|
||||||
|
},
|
||||||
|
loop_control: {
|
||||||
|
max_retries_per_step: 3,
|
||||||
|
reserved_context_size: 50000,
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
keep_alive_on_exit: false,
|
||||||
|
},
|
||||||
|
permission: { rules: [] },
|
||||||
|
hooks: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> {
|
||||||
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSection<T>(rawOther: unknown, key: string): T | undefined {
|
||||||
|
const root = asRecord(rawOther);
|
||||||
|
const section = root[key];
|
||||||
|
if (section === undefined || section === null) return undefined;
|
||||||
|
return section as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAgentSettings(rawOther: unknown): AgentSettings {
|
||||||
|
return {
|
||||||
|
thinking: {
|
||||||
|
...DEFAULT_SETTINGS.thinking,
|
||||||
|
...getSection<AgentSettings["thinking"]>(rawOther, "thinking"),
|
||||||
|
},
|
||||||
|
loop_control: {
|
||||||
|
...DEFAULT_SETTINGS.loop_control,
|
||||||
|
...getSection<AgentSettings["loop_control"]>(rawOther, "loop_control"),
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
...DEFAULT_SETTINGS.background,
|
||||||
|
...getSection<AgentSettings["background"]>(rawOther, "background"),
|
||||||
|
},
|
||||||
|
permission: {
|
||||||
|
rules: getSection<AgentSettings["permission"]>(rawOther, "permission")?.rules ?? [],
|
||||||
|
},
|
||||||
|
hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAgentSettings(
|
||||||
|
rawOther: unknown,
|
||||||
|
patch: Partial<AgentSettings>
|
||||||
|
): unknown {
|
||||||
|
const root = { ...asRecord(rawOther) };
|
||||||
|
const current = getAgentSettings(rawOther);
|
||||||
|
const next: AgentSettings = {
|
||||||
|
thinking: { ...current.thinking, ...patch.thinking },
|
||||||
|
loop_control: { ...current.loop_control, ...patch.loop_control },
|
||||||
|
background: { ...current.background, ...patch.background },
|
||||||
|
permission: { rules: patch.permission?.rules ?? current.permission?.rules ?? [] },
|
||||||
|
hooks: patch.hooks ?? current.hooks ?? [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (next.thinking) root.thinking = next.thinking;
|
||||||
|
if (next.loop_control) root.loop_control = next.loop_control;
|
||||||
|
if (next.background) root.background = next.background;
|
||||||
|
if (next.permission?.rules && next.permission.rules.length > 0) {
|
||||||
|
root.permission = next.permission;
|
||||||
|
} else {
|
||||||
|
delete root.permission;
|
||||||
|
}
|
||||||
|
if (next.hooks && next.hooks.length > 0) {
|
||||||
|
root.hooks = next.hooks;
|
||||||
|
} else {
|
||||||
|
delete root.hooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/lib/agent-settings.ts
|
||||||
|
git commit -m "feat: add agent settings read/write helpers"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Add i18n keys
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/i18n/zh.ts`
|
||||||
|
- Modify: `src/i18n/en.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add Chinese keys**
|
||||||
|
|
||||||
|
Insert into `src/i18n/zh.ts` in the "Agent / target" section:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
agentSettings: "全局配置",
|
||||||
|
enableThinking: "启用思考",
|
||||||
|
thinkingLevel: "思考等级",
|
||||||
|
thinkingKeep: "保留思考内容",
|
||||||
|
thinkingLow: "低",
|
||||||
|
thinkingMedium: "中",
|
||||||
|
thinkingHigh: "高",
|
||||||
|
thinkingMax: "最大",
|
||||||
|
thinkingContextHint: "启用思考会占用更多上下文,请确保模型上下文长度和预留空间足够。",
|
||||||
|
loopControlSettings: "循环控制",
|
||||||
|
maxRetriesPerStep: "单步重试次数",
|
||||||
|
reservedContextSize: "上下文预留大小",
|
||||||
|
backgroundSettings: "后台任务",
|
||||||
|
maxRunningTasks: "最大并发数",
|
||||||
|
keepAliveOnExit: "退出时保持运行",
|
||||||
|
permissionRules: "权限规则",
|
||||||
|
permissionDecision: "处置",
|
||||||
|
permissionPattern: "模式",
|
||||||
|
permissionAllow: "允许",
|
||||||
|
permissionDeny: "拒绝",
|
||||||
|
permissionAsk: "询问",
|
||||||
|
addRule: "+ 添加规则",
|
||||||
|
addCommonRules: "添加常用规则",
|
||||||
|
hooks: "生命周期钩子",
|
||||||
|
hookEvent: "事件",
|
||||||
|
hookMatcher: "匹配器",
|
||||||
|
hookCommand: "命令",
|
||||||
|
hookTimeout: "超时",
|
||||||
|
addHook: "+ 添加钩子",
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add English keys**
|
||||||
|
|
||||||
|
Insert matching keys into `src/i18n/en.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
agentSettings: "Global Settings",
|
||||||
|
enableThinking: "Enable thinking",
|
||||||
|
thinkingLevel: "Thinking level",
|
||||||
|
thinkingKeep: "Keep thinking content",
|
||||||
|
thinkingLow: "Low",
|
||||||
|
thinkingMedium: "Medium",
|
||||||
|
thinkingHigh: "High",
|
||||||
|
thinkingMax: "Max",
|
||||||
|
thinkingContextHint: "Thinking uses more context. Ensure the model context length and reserved size are sufficient.",
|
||||||
|
loopControlSettings: "Loop Control",
|
||||||
|
maxRetriesPerStep: "Max retries per step",
|
||||||
|
reservedContextSize: "Reserved context size",
|
||||||
|
backgroundSettings: "Background Tasks",
|
||||||
|
maxRunningTasks: "Max running tasks",
|
||||||
|
keepAliveOnExit: "Keep alive on exit",
|
||||||
|
permissionRules: "Permission Rules",
|
||||||
|
permissionDecision: "Decision",
|
||||||
|
permissionPattern: "Pattern",
|
||||||
|
permissionAllow: "Allow",
|
||||||
|
permissionDeny: "Deny",
|
||||||
|
permissionAsk: "Ask",
|
||||||
|
addRule: "+ Add rule",
|
||||||
|
addCommonRules: "Add common rules",
|
||||||
|
hooks: "Lifecycle Hooks",
|
||||||
|
hookEvent: "Event",
|
||||||
|
hookMatcher: "Matcher",
|
||||||
|
hookCommand: "Command",
|
||||||
|
hookTimeout: "Timeout",
|
||||||
|
addHook: "+ Add hook",
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/i18n/zh.ts src/i18n/en.ts
|
||||||
|
git commit -m "i18n: add agent settings panel translations"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Build the AgentSettingsPanel component
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/components/AgentSettingsPanel.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the panel component**
|
||||||
|
|
||||||
|
Create `src/components/AgentSettingsPanel.tsx`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTranslation } from "../i18n";
|
||||||
|
import { getAgentSettings, setAgentSettings } from "../lib/agent-settings";
|
||||||
|
import type { AgentSettings, PermissionRule, Hook } from "../types";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { TranslationKey } from "../i18n/zh";
|
||||||
|
|
||||||
|
interface AgentSettingsPanelProps {
|
||||||
|
rawOther: unknown;
|
||||||
|
onChange: (nextRawOther: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const THINKING_LEVELS = ["low", "medium", "high", "max"] as const;
|
||||||
|
const THINKING_LABELS: Record<(typeof THINKING_LEVELS)[number], TranslationKey> = {
|
||||||
|
low: "thinkingLow",
|
||||||
|
medium: "thinkingMedium",
|
||||||
|
high: "thinkingHigh",
|
||||||
|
max: "thinkingMax",
|
||||||
|
};
|
||||||
|
const PERMISSION_DECISIONS = ["allow", "deny", "ask"] as const;
|
||||||
|
const PERMISSION_LABELS: Record<(typeof PERMISSION_DECISIONS)[number], TranslationKey> = {
|
||||||
|
allow: "permissionAllow",
|
||||||
|
deny: "permissionDeny",
|
||||||
|
ask: "permissionAsk",
|
||||||
|
};
|
||||||
|
const COMMON_EVENTS = ["PreToolUse", "PostToolUse"] as const;
|
||||||
|
|
||||||
|
export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const settings = getAgentSettings(rawOther);
|
||||||
|
|
||||||
|
const update = (patch: Partial<AgentSettings>) => {
|
||||||
|
onChange(setAgentSettings(rawOther, patch));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateThinking = (patch: Partial<AgentSettings["thinking"]>) => {
|
||||||
|
update({ thinking: { ...settings.thinking, ...patch } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLoopControl = (patch: Partial<AgentSettings["loop_control"]>) => {
|
||||||
|
update({ loop_control: { ...settings.loop_control, ...patch } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBackground = (patch: Partial<AgentSettings["background"]>) => {
|
||||||
|
update({ background: { ...settings.background, ...patch } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setRules = (rules: PermissionRule[]) => {
|
||||||
|
update({ permission: { rules } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setHooks = (hooks: Hook[]) => {
|
||||||
|
update({ hooks });
|
||||||
|
};
|
||||||
|
|
||||||
|
const thinkingEnabled = settings.thinking?.enabled ?? true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
<h3 className="text-gray-400 text-sm font-medium">{t("agentSettings")}</h3>
|
||||||
|
|
||||||
|
{/* Thinking */}
|
||||||
|
<Card title={t("enableThinking")}>
|
||||||
|
<Checkbox
|
||||||
|
label={t("enableThinking")}
|
||||||
|
checked={thinkingEnabled}
|
||||||
|
onChange={(checked) => updateThinking({ enabled: checked })}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<span className="text-sm text-gray-400">{t("thinkingLevel")}</span>
|
||||||
|
<Segmented
|
||||||
|
options={THINKING_LEVELS.map((lvl) => ({ key: lvl, label: t(THINKING_LABELS[lvl]) }))}
|
||||||
|
value={settings.thinking?.effort ?? "medium"}
|
||||||
|
onChange={(effort) => updateThinking({ effort })}
|
||||||
|
disabled={!thinkingEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
label={t("thinkingKeep")}
|
||||||
|
checked={settings.thinking?.keep === "all"}
|
||||||
|
disabled={!thinkingEnabled}
|
||||||
|
onChange={(checked) => updateThinking({ keep: checked ? "all" : false })}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">{t("thinkingContextHint")}</p>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Loop Control */}
|
||||||
|
<Card title={t("loopControlSettings")}>
|
||||||
|
<NumberField
|
||||||
|
label={t("maxRetriesPerStep")}
|
||||||
|
value={settings.loop_control?.max_retries_per_step ?? 3}
|
||||||
|
onChange={(v) => updateLoopControl({ max_retries_per_step: v })}
|
||||||
|
/>
|
||||||
|
<NumberField
|
||||||
|
label={t("reservedContextSize")}
|
||||||
|
value={settings.loop_control?.reserved_context_size ?? 50000}
|
||||||
|
onChange={(v) => updateLoopControl({ reserved_context_size: v })}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Background */}
|
||||||
|
<Card title={t("backgroundSettings")}>
|
||||||
|
<NumberField
|
||||||
|
label={t("maxRunningTasks")}
|
||||||
|
value={settings.background?.max_running_tasks}
|
||||||
|
onChange={(v) => updateBackground({ max_running_tasks: v })}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t("keepAliveOnExit")}
|
||||||
|
checked={settings.background?.keep_alive_on_exit ?? false}
|
||||||
|
onChange={(checked) => updateBackground({ keep_alive_on_exit: checked })}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Permission Rules */}
|
||||||
|
<Card title={t("permissionRules")}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(settings.permission?.rules ?? []).map((rule, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2">
|
||||||
|
<select
|
||||||
|
className="bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={rule.decision ?? "allow"}
|
||||||
|
onChange={(e) => {
|
||||||
|
const rules = [...(settings.permission?.rules ?? [])];
|
||||||
|
rules[idx] = { ...rule, decision: e.target.value as PermissionRule["decision"] };
|
||||||
|
setRules(rules);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{PERMISSION_DECISIONS.map((d) => (
|
||||||
|
<option key={d} value={d}>{t(PERMISSION_LABELS[d])}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 min-w-0 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={rule.pattern ?? ""}
|
||||||
|
placeholder={t("permissionPattern")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const rules = [...(settings.permission?.rules ?? [])];
|
||||||
|
rules[idx] = { ...rule, pattern: e.target.value };
|
||||||
|
setRules(rules);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const rules = [...(settings.permission?.rules ?? [])];
|
||||||
|
rules.splice(idx, 1);
|
||||||
|
setRules(rules);
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 text-sm text-red-400 hover:bg-red-900/20 rounded"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setRules([...(settings.permission?.rules ?? []), { decision: "allow", pattern: "" }]);
|
||||||
|
}}
|
||||||
|
className="px-3 py-1.5 text-sm border border-[#2a2a2e] rounded hover:bg-[#252529]"
|
||||||
|
>
|
||||||
|
{t("addRule")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setRules([
|
||||||
|
...(settings.permission?.rules ?? []),
|
||||||
|
{ decision: "allow", pattern: "Read" },
|
||||||
|
{ decision: "deny", pattern: "Bash(rm -rf*)" },
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
className="px-3 py-1.5 text-sm border border-[#2a2a2e] rounded hover:bg-[#252529]"
|
||||||
|
>
|
||||||
|
{t("addCommonRules")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Hooks */}
|
||||||
|
<Card title={t("hooks")}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(settings.hooks ?? []).map((hook, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2 flex-wrap">
|
||||||
|
<input
|
||||||
|
list="hook-events"
|
||||||
|
type="text"
|
||||||
|
className="w-32 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={hook.event ?? ""}
|
||||||
|
placeholder={t("hookEvent")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const hooks = [...(settings.hooks ?? [])];
|
||||||
|
hooks[idx] = { ...hook, event: e.target.value };
|
||||||
|
setHooks(hooks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<datalist id="hook-events">
|
||||||
|
{COMMON_EVENTS.map((e) => <option key={e} value={e} />)}
|
||||||
|
</datalist>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-24 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={hook.matcher ?? ""}
|
||||||
|
placeholder={t("hookMatcher")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const hooks = [...(settings.hooks ?? [])];
|
||||||
|
hooks[idx] = { ...hook, matcher: e.target.value };
|
||||||
|
setHooks(hooks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 min-w-0 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={hook.command ?? ""}
|
||||||
|
placeholder={t("hookCommand")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const hooks = [...(settings.hooks ?? [])];
|
||||||
|
hooks[idx] = { ...hook, command: e.target.value };
|
||||||
|
setHooks(hooks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="w-20 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={hook.timeout ?? ""}
|
||||||
|
placeholder={t("hookTimeout")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const hooks = [...(settings.hooks ?? [])];
|
||||||
|
hooks[idx] = { ...hook, timeout: e.target.value === "" ? undefined : Number(e.target.value) };
|
||||||
|
setHooks(hooks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const hooks = [...(settings.hooks ?? [])];
|
||||||
|
hooks.splice(idx, 1);
|
||||||
|
setHooks(hooks);
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 text-sm text-red-400 hover:bg-red-900/20 rounded"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setHooks([...(settings.hooks ?? []), { event: "", matcher: "", command: "" }]);
|
||||||
|
}}
|
||||||
|
className="px-3 py-1.5 text-sm border border-[#2a2a2e] rounded hover:bg-[#252529]"
|
||||||
|
>
|
||||||
|
{t("addHook")}
|
||||||
|
</button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Card({ title, children }: { title: string; children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#16161a] border border-[#2a2a2e] rounded-xl p-4 space-y-3">
|
||||||
|
<h4 className="text-gray-400 text-sm font-medium">{title}</h4>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
label,
|
||||||
|
checked,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
checked: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className={`flex items-center gap-2 text-sm ${disabled ? "text-gray-600" : "text-[#e5e5e7]"}`}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
|
className="rounded border-[#2a2a2e] bg-[#1f1f23] text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumberField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value?: number;
|
||||||
|
onChange: (value: number | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="flex items-center gap-3 text-sm text-[#e5e5e7]">
|
||||||
|
<span className="text-gray-400">{label}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value;
|
||||||
|
onChange(raw === "" ? undefined : Number(raw));
|
||||||
|
}}
|
||||||
|
className="w-32 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Segmented({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
options: { key: string; label: string }[];
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center rounded overflow-hidden border border-[#2a2a2e] ${disabled ? "opacity-50" : ""}`}>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.key}
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => onChange(opt.key)}
|
||||||
|
className={`px-3 py-1 text-sm ${
|
||||||
|
value === opt.key
|
||||||
|
? "bg-blue-600 text-white"
|
||||||
|
: "bg-[#1f1f23] text-gray-400 hover:bg-[#252529]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/components/AgentSettingsPanel.tsx
|
||||||
|
git commit -m "feat: add AgentSettingsPanel component"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Wire AgentSettingsPanel into ProviderEdit
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/components/ProviderEdit.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update ProviderEditProps**
|
||||||
|
|
||||||
|
Add `rawOther` and `onRawOtherChange` to the props interface:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface ProviderEditProps {
|
||||||
|
agent: Agent;
|
||||||
|
provider: Provider;
|
||||||
|
models: Model[];
|
||||||
|
defaultModel: string | null;
|
||||||
|
rawOther: unknown;
|
||||||
|
onRawOtherChange: (nextRawOther: unknown) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
// ... existing callbacks
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Destructure new props**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export function ProviderEdit({
|
||||||
|
agent,
|
||||||
|
provider,
|
||||||
|
models,
|
||||||
|
defaultModel,
|
||||||
|
rawOther,
|
||||||
|
onRawOtherChange,
|
||||||
|
onBack,
|
||||||
|
// ... existing
|
||||||
|
}: ProviderEditProps) {
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Render the panel in the model mapping tab**
|
||||||
|
|
||||||
|
Locate the model mapping tab content (around the model table and "+ 添加模型映射" button) and insert `<AgentSettingsPanel />` at the bottom, conditionally for Kimi Code:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{agent === "kimi_code" && (
|
||||||
|
<AgentSettingsPanel rawOther={rawOther} onChange={onRawOtherChange} />
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/components/ProviderEdit.tsx
|
||||||
|
git commit -m "feat: render AgentSettingsPanel in ProviderEdit for Kimi Code"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Wire ProviderEdit to App.tsx
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/App.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Pass rawOther and updater to ProviderEdit**
|
||||||
|
|
||||||
|
Find the `<ProviderEdit ... />` call and add:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ProviderEdit
|
||||||
|
agent={agent}
|
||||||
|
provider={currentProvider}
|
||||||
|
models={...}
|
||||||
|
defaultModel={config.default_model}
|
||||||
|
rawOther={config.raw_other}
|
||||||
|
onRawOtherChange={(nextRawOther) =>
|
||||||
|
updateConfig((cfg) => ({ ...cfg, raw_other: nextRawOther }))
|
||||||
|
}
|
||||||
|
// ... existing props
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/App.tsx
|
||||||
|
git commit -m "chore: pass rawOther to ProviderEdit"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Apply smart max_context_size defaults when adding models
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/App.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Import helper in App.tsx**
|
||||||
|
|
||||||
|
In `src/App.tsx`, import:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { getDefaultMaxContextSize } from "./lib/model-defaults";
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Use lookup in onModelAdd**
|
||||||
|
|
||||||
|
Find the `onModelAdd` prop passed to `<ProviderEdit />` and update the default `max_context_size`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
onModelAdd={() => {
|
||||||
|
const providerModels = Object.values(config.models).filter(
|
||||||
|
(m) => m.provider === currentProvider.name
|
||||||
|
);
|
||||||
|
const alias = `${currentProvider.name}-${providerModels.length + 1}`;
|
||||||
|
updateConfig((cfg) => ({
|
||||||
|
...cfg,
|
||||||
|
models: {
|
||||||
|
...cfg.models,
|
||||||
|
[alias]: {
|
||||||
|
alias,
|
||||||
|
provider: currentProvider.name,
|
||||||
|
model: "",
|
||||||
|
max_context_size: getDefaultMaxContextSize(alias),
|
||||||
|
display_name: null,
|
||||||
|
role: null,
|
||||||
|
supports_1m: false,
|
||||||
|
capabilities: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Use lookup in handleApplyProviderJson**
|
||||||
|
|
||||||
|
Update the model creation loop in `handleApplyProviderJson`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
updatedModels[alias] = {
|
||||||
|
...m,
|
||||||
|
alias,
|
||||||
|
provider: provider.name,
|
||||||
|
max_context_size: m.max_context_size ?? getDefaultMaxContextSize(m.model),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/App.tsx
|
||||||
|
git commit -m "feat: apply smart default max_context_size when adding models"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Verify with build and manual checks
|
||||||
|
|
||||||
|
- [ ] **Step 1: Type check**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `tsc` and Vite build succeed.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the app**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run tauri-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Manual verification**
|
||||||
|
|
||||||
|
1. Kimi Code tab → edit a provider → "模型映射" tab → scroll to bottom.
|
||||||
|
2. Confirm "全局配置" panel shows 5 cards.
|
||||||
|
3. Toggle "启用思考" and verify effort/keep enable/disable.
|
||||||
|
4. Change thinking effort, save, open `~/.kimi-code/config.toml`, verify `[thinking]` block.
|
||||||
|
5. Add a permission rule and a hook, save, verify `[[permission.rules]]` and `[[hooks]]` in TOML.
|
||||||
|
6. Add a new model with alias `glm-5.2-xxx`, verify `max_context_size` defaults to `1000000`.
|
||||||
|
7. Add a new model with unknown alias, verify `max_context_size` defaults to `256000`.
|
||||||
|
8. Switch to Pi tab, confirm global settings panel is not visible.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit any fixes**
|
||||||
|
|
||||||
|
If any fixes were needed during verification, commit them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- **Spec coverage:** Every requirement (5 cards, model defaults, i18n, Kimi-only visibility, manual override) maps to a task.
|
||||||
|
- **Placeholder scan:** No TBD/TODO/fill-in-details; all code is shown.
|
||||||
|
- **Type consistency:** `AgentSettings`, helper signatures, and component props match across tasks.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Kimi Code 选项卡 /reload 提示设计
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
用户在 Kimi Code 选项卡下切换供应商后,Kimi Code CLI 需要执行 `/reload` 才能加载新的提供方配置。当前界面没有给出该提示,用户容易遗漏这一步。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在 Kimi Code 选项卡下,给每个供应商的"切换使用"按钮旁边增加一个信息提示,告知用户切换供应商后需要执行 `/reload`。
|
||||||
|
|
||||||
|
## 设计
|
||||||
|
|
||||||
|
### 位置与交互
|
||||||
|
|
||||||
|
- 在 `ProviderList` 组件中,每个供应商卡片右侧的"切换使用"按钮旁边添加一个信息图标 `ⓘ`。
|
||||||
|
- 使用原生 `title` 属性展示 tooltip,鼠标悬停时显示提示文案。
|
||||||
|
- 仅在当前选中 `agent === "kimi_code"` 时显示该图标;Pi 选项卡下不显示。
|
||||||
|
|
||||||
|
### 文案
|
||||||
|
|
||||||
|
- 中文:`切换供应商后,请在 Kimi Code 中执行 /reload 以生效。`
|
||||||
|
- 英文:`After switching providers, run /reload in Kimi Code to apply the change.`
|
||||||
|
|
||||||
|
### 样式
|
||||||
|
|
||||||
|
- 图标大小约 14px,颜色为 `text-gray-500`,hover 时变为 `text-gray-300`。
|
||||||
|
- 图标与"切换使用"按钮间距约 8px(`gap-2`)。
|
||||||
|
- 不引入额外依赖或自定义 tooltip 组件。
|
||||||
|
|
||||||
|
### 组件与数据流
|
||||||
|
|
||||||
|
- `src/components/ProviderList.tsx`:
|
||||||
|
- `ProviderListProps` 新增 `agent: Agent` 字段。
|
||||||
|
- 根据 `agent` 决定是否渲染提示图标。
|
||||||
|
- 图标 `title` 使用 `t("switchReloadHint")`。
|
||||||
|
- `src/App.tsx`:
|
||||||
|
- 渲染 `<ProviderList />` 时传入 `agent={agent}`。
|
||||||
|
- `src/i18n/zh.ts` 与 `src/i18n/en.ts`:
|
||||||
|
- 新增键 `switchReloadHint`。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- [ ] 切换到 Kimi Code 选项卡时,每个供应商的"切换使用"按钮旁出现 `ⓘ` 图标。
|
||||||
|
- [ ] 鼠标悬停在图标上时,显示正确语言版本的 `/reload` 提示。
|
||||||
|
- [ ] 切换到 Pi 选项卡时,该图标不显示。
|
||||||
|
- [ ] 不引入新的运行时依赖。
|
||||||
|
- [ ] TypeScript 编译与项目构建保持通过。
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
# Kimi Code 全局配置面板设计
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
Kimi Code CLI 的 `~/.kimi-code/config.toml` 包含大量全局设置:`[thinking]`、`[loop_control]`、`[background]`、`[[permission.rules]]`、`[[hooks]]` 等。当前 Pi Switch 只暴露了 `providers` 和 `models` 的可视化编辑,其他全局配置只能通过"配置 JSON"标签页手动修改,不够便捷。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
1. 在 Kimi Code 选项卡的供应商编辑页面(`ProviderEdit`)中,"模型映射"标签页下方新增一个"全局配置"面板,用图形化控件快速编辑上述 Kimi Code 全局设置。
|
||||||
|
2. 根据模型 ID 自动为新添加/发现的模型设置默认 `max_context_size`,全局默认值为 256000,具体模型按可扩展的键值对表格匹配。
|
||||||
|
|
||||||
|
模型级的 `max_context_size` 已在模型映射表格中实现,用户仍可手动覆盖自动默认值。
|
||||||
|
|
||||||
|
## 设计
|
||||||
|
|
||||||
|
### 位置
|
||||||
|
|
||||||
|
- 组件:`src/components/ProviderEdit.tsx`
|
||||||
|
- 仅在 `agent === "kimi_code"` 时显示。
|
||||||
|
- 位于"模型映射"标签页的内容区域内,在模型映射表格和"+ 添加模型映射"按钮的下方。
|
||||||
|
- 垂直排列,超出区域高度时由页面滚动条处理。
|
||||||
|
|
||||||
|
### 分组与控件
|
||||||
|
|
||||||
|
面板按功能分为 5 张卡片:
|
||||||
|
|
||||||
|
#### 卡片 1:思考模式 `[thinking]`
|
||||||
|
|
||||||
|
| 控件 | 字段 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 复选框 | `enabled` | 是否默认开启思考 |
|
||||||
|
| 分段按钮 | `effort` | 思考强度:`low` / `medium` / `high` / `max` |
|
||||||
|
| 复选框 | `keep` | 是否保留历史思考内容(`"all"` 或关值) |
|
||||||
|
|
||||||
|
- 思考等级和保留思考内容仅在"启用思考"勾选时可用,禁用时置灰。
|
||||||
|
- "保留思考内容"未勾选时,写入 `[thinking].keep = false`(不能省略,否则 CLI 会回退到默认值 `"all"`)。
|
||||||
|
- 卡片底部小字提示:"启用思考会占用更多上下文,请确保模型上下文长度和预留空间足够。"
|
||||||
|
|
||||||
|
#### 卡片 2:循环控制 `[loop_control]`
|
||||||
|
|
||||||
|
| 控件 | 字段 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 数字输入框 | `max_retries_per_step` | 单步失败后最大重试次数,默认 3 |
|
||||||
|
| 数字输入框 | `reserved_context_size` | 预留给模型输出的 token 数,单位 token |
|
||||||
|
|
||||||
|
#### 卡片 3:后台任务 `[background]`
|
||||||
|
|
||||||
|
| 控件 | 字段 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 数字输入框 | `max_running_tasks` | 同时运行的最大后台任务数 |
|
||||||
|
| 复选框 | `keep_alive_on_exit` | 会话关闭时是否保留后台任务 |
|
||||||
|
|
||||||
|
#### 卡片 4:权限规则 `[[permission.rules]]`
|
||||||
|
|
||||||
|
- 可添加/删除的规则列表,默认空列表。
|
||||||
|
- 每行包含:
|
||||||
|
- **处置** 下拉框:`allow` / `deny` / `ask`
|
||||||
|
- **模式** 输入框:如 `Read`、`Bash(rm -rf*)`
|
||||||
|
- **删除** 按钮
|
||||||
|
- 提供"添加常用规则"快捷入口,可一键插入:
|
||||||
|
- `allow` + `Read`
|
||||||
|
- `deny` + `Bash(rm -rf*)`
|
||||||
|
|
||||||
|
#### 卡片 5:生命周期钩子 `[[hooks]]`
|
||||||
|
|
||||||
|
- 可添加/删除的钩子列表,默认空列表。
|
||||||
|
- 每行包含:
|
||||||
|
- **事件** 下拉框:常见事件如 `PreToolUse` / `PostToolUse`,也可手动输入其他事件
|
||||||
|
- **匹配器** 输入框:如 `Bash`
|
||||||
|
- **命令** 输入框:如 `node ~/.kimi-code/hooks/check-bash.mjs`
|
||||||
|
- **超时** 数字输入框(秒)
|
||||||
|
- **删除** 按钮
|
||||||
|
|
||||||
|
### 样式
|
||||||
|
|
||||||
|
- 卡片容器:`bg-[#16161a] border border-[#2a2a2e] rounded-xl p-4`
|
||||||
|
- 卡片间距:`gap-4`
|
||||||
|
- 卡片标题:`text-gray-400 text-sm font-medium mb-3`
|
||||||
|
- 表单项:`flex items-center gap-4 flex-wrap`
|
||||||
|
- 数字输入框宽度:`w-32`
|
||||||
|
- 禁用状态:文字 `text-gray-600`,按钮不可交互
|
||||||
|
|
||||||
|
### 字段默认值
|
||||||
|
|
||||||
|
当 `raw_other` 中不存在对应表时,控件按以下默认值展示:
|
||||||
|
|
||||||
|
| 配置块 | 字段 | UI 默认值 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `[thinking]` | `enabled` | `true` |
|
||||||
|
| `[thinking]` | `effort` | `"medium"` |
|
||||||
|
| `[thinking]` | `keep` | `"all"`(勾选) |
|
||||||
|
| `[loop_control]` | `max_retries_per_step` | `3` |
|
||||||
|
| `[loop_control]` | `reserved_context_size` | `50000` |
|
||||||
|
| `[background]` | `max_running_tasks` | 空(不写入) |
|
||||||
|
| `[background]` | `keep_alive_on_exit` | `false` |
|
||||||
|
| `[[permission.rules]]` | — | 空列表 |
|
||||||
|
| `[[hooks]]` | — | 空列表 |
|
||||||
|
|
||||||
|
数字输入框允许清空,表示不写入配置文件,由 Kimi Code CLI 使用其内置默认值。
|
||||||
|
|
||||||
|
### 数据流
|
||||||
|
|
||||||
|
- `App.tsx` 向 `ProviderEdit` 传入:
|
||||||
|
- `agent`
|
||||||
|
- `rawOther`(即 `config.raw_other`)
|
||||||
|
- `updateRawOther: (updater: (rawOther: unknown) => unknown) => void`
|
||||||
|
- `ProviderEdit` 中新增内部子组件 `AgentSettingsPanel`,封装所有全局配置 UI。
|
||||||
|
- 新增 TypeScript 类型(`src/types/index.ts`):
|
||||||
|
- `ThinkingConfig`
|
||||||
|
- `LoopControlConfig`
|
||||||
|
- `BackgroundConfig`
|
||||||
|
- `PermissionRule`
|
||||||
|
- `Hook`
|
||||||
|
- `AgentSettings`
|
||||||
|
- 新增 helper(`src/components/AgentSettingsPanel.tsx` 或 `src/lib/agent-settings.ts`):
|
||||||
|
- `getAgentSettings(rawOther: unknown): AgentSettings`
|
||||||
|
- `setAgentSettings(rawOther: unknown, patch: Partial<AgentSettings>): unknown`
|
||||||
|
- 统一从 `raw_other` 中读取/写入 `[thinking]`、`[loop_control]`、`[background]`、`[[permission.rules]]`、`[[hooks]]`。
|
||||||
|
- 后端无需改动:这些顶层表原本就通过 `Config.raw_other` 在导入/导出时保留。
|
||||||
|
|
||||||
|
### 模型默认上下文长度
|
||||||
|
|
||||||
|
在新增模型(手动添加、一键设置、从 API 发现模型)时,根据模型 ID 自动推荐 `max_context_size`。
|
||||||
|
|
||||||
|
- 默认回退值:`256000`
|
||||||
|
- 匹配方式:按模型 ID 字符串进行不区分大小写的正则前缀/全名匹配(代码中使用 `/pattern/i`),命中第一条规则即返回。
|
||||||
|
- 规则表(按匹配优先级排列):
|
||||||
|
|
||||||
|
| 匹配规则(不区分大小写) | `max_context_size` | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **国内模型(优先)** | | |
|
||||||
|
| `^kimi-for-coding$` | `262144` | Kimi Code 官方托管模型 |
|
||||||
|
| `^kimi-k2\.5` | `256000` | Kimi K2.5 系列 |
|
||||||
|
| `^kimi-k2` | `256000` | Kimi K2 系列 |
|
||||||
|
| `^kimi-` | `256000` | 其他 Kimi 模型 |
|
||||||
|
| `^glm-5\.2` | `1000000` | GLM-5.2 |
|
||||||
|
| `^glm-5\.1` | `256000` | GLM-5.1 |
|
||||||
|
| `^glm-5` | `256000` | GLM-5 系列 |
|
||||||
|
| `^glm-4` | `128000` | GLM-4 / GLM-4-Plus / GLM-4-Flash |
|
||||||
|
| `^glm-` | `128000` | 其他 GLM 模型 |
|
||||||
|
| `^MiniMax-M3` | `1000000` | MiniMax-M3 |
|
||||||
|
| `^MiniMax-Text-01` | `400000` | MiniMax-Text-01 |
|
||||||
|
| `^MiniMax-` | `256000` | 其他 MiniMax 模型 |
|
||||||
|
| `^qwen2\.5` | `128000` | 通义千问 Qwen2.5 系列 |
|
||||||
|
| `^qwen-max` | `128000` | 通义千问 Max |
|
||||||
|
| `^qwen-plus` | `128000` | 通义千问 Plus |
|
||||||
|
| `^qwen-turbo` | `128000` | 通义千问 Turbo |
|
||||||
|
| `^qwen-coder` | `128000` | 通义千问 Coder |
|
||||||
|
| `^qwen-` | `128000` | 其他通义千问模型 |
|
||||||
|
| `^deepseek-r1` | `64000` | DeepSeek-R1 |
|
||||||
|
| `^deepseek-v3` | `64000` | DeepSeek-V3 |
|
||||||
|
| `^deepseek-coder` | `64000` | DeepSeek-Coder |
|
||||||
|
| `^deepseek-` | `64000` | 其他 DeepSeek 模型 |
|
||||||
|
| `^hunyuan-pro` | `32000` | 腾讯 Hunyuan Pro |
|
||||||
|
| `^hunyuan-standard` | `32000` | 腾讯 Hunyuan Standard |
|
||||||
|
| `^hunyuan-lite` | `32000` | 腾讯 Hunyuan Lite |
|
||||||
|
| `^hunyuan-` | `32000` | 其他 Hunyuan 模型 |
|
||||||
|
| `^doubao-pro` | `128000` | 字节 Doubao Pro |
|
||||||
|
| `^doubao-lite` | `128000` | 字节 Doubao Lite |
|
||||||
|
| `^doubao-vision` | `128000` | 字节 Doubao Vision |
|
||||||
|
| `^doubao-` | `128000` | 其他 Doubao 模型 |
|
||||||
|
| `^ernie-4\.0` | `128000` | 百度文心 4.0 |
|
||||||
|
| `^ernie-3\.5` | `128000` | 百度文心 3.5 |
|
||||||
|
| `^ernie-speed` | `128000` | 百度文心 Speed |
|
||||||
|
| `^ernie-lite` | `128000` | 百度文心 Lite |
|
||||||
|
| `^ernie-` | `128000` | 其他文心模型 |
|
||||||
|
| `^spark-v4` | `32000` | 讯飞星火 V4 |
|
||||||
|
| `^spark-v3\.5` | `32000` | 讯飞星火 V3.5 |
|
||||||
|
| `^spark-pro` | `32000` | 讯飞星火 Pro |
|
||||||
|
| `^spark-max` | `32000` | 讯飞星火 Max |
|
||||||
|
| `^spark-` | `32000` | 其他星火模型 |
|
||||||
|
| `^sensechat-` | `128000` | 商汤 SenseChat |
|
||||||
|
| `^baichuan-4` | `128000` | 百川 Baichuan 4 |
|
||||||
|
| `^baichuan-3` | `128000` | 百川 Baichuan 3 |
|
||||||
|
| `^baichuan-` | `128000` | 其他百川模型 |
|
||||||
|
| `^yi-` | `128000` | 零一万物 Yi 系列 |
|
||||||
|
| **国际模型** | | |
|
||||||
|
| `^claude-opus` | `200000` | Claude Opus 系列(多数版本为 200K;Claude 4 部分版本可达 1M,这里取保守值) |
|
||||||
|
| `^claude-sonnet` | `200000` | Claude Sonnet 系列 |
|
||||||
|
| `^claude-haiku` | `200000` | Claude Haiku 系列 |
|
||||||
|
| `^claude-` | `200000` | 其他 Claude 模型 |
|
||||||
|
| `^gpt-4\.1` | `1047576` | GPT-4.1 系列(1M tokens) |
|
||||||
|
| `^gpt-4o` | `128000` | GPT-4o 系列 |
|
||||||
|
| `^gpt-4-turbo` | `128000` | GPT-4 Turbo |
|
||||||
|
| `^gpt-4-` | `128000` | 其他 GPT-4 模型 |
|
||||||
|
| `^gemini-2\.0-flash` | `1048576` | Gemini 2.0 Flash(1M tokens) |
|
||||||
|
| `^gemini-1\.5-pro` | `2097152` | Gemini 1.5 Pro(2M tokens) |
|
||||||
|
| `^gemini-1\.5-flash` | `1048576` | Gemini 1.5 Flash(1M tokens) |
|
||||||
|
| `^gemini-` | `1048576` | 其他 Gemini 模型 |
|
||||||
|
| (默认) | `256000` | 未命中任何规则 |
|
||||||
|
|
||||||
|
> 注:模型上下文长度取公开资料典型值,部分国内/国际模型存在多个版本导致数值差异,实际以供应商文档为准。规则表以代码常量形式存在,按 specificity 从高到低排列,命中第一条即返回,未命中时回退到 256000。用户仍可在模型映射表格中手动覆盖。
|
||||||
|
|
||||||
|
- 实现位置:`src/lib/model-defaults.ts`(或类似纯函数文件)。
|
||||||
|
- 核心函数:
|
||||||
|
```ts
|
||||||
|
export function getDefaultMaxContextSize(modelId: string): number;
|
||||||
|
```
|
||||||
|
- 调用点:
|
||||||
|
- `ProviderEdit` 中手动添加模型时。
|
||||||
|
- `ProviderEdit` 中通过"获取模型列表"发现模型并添加时。
|
||||||
|
- `handleAddProvider` / `handleApplyProviderJson` 等生成默认模型的地方,可一并使用此函数作为更智能的默认值。
|
||||||
|
|
||||||
|
### i18n 键
|
||||||
|
|
||||||
|
新增键:
|
||||||
|
|
||||||
|
- `agentSettings`: "全局配置"
|
||||||
|
- `enableThinking`: "启用思考"
|
||||||
|
- `thinkingLevel`: "思考等级"
|
||||||
|
- `thinkingKeep`: "保留思考内容"
|
||||||
|
- `thinkingLow`: "低"
|
||||||
|
- `thinkingMedium`: "中"
|
||||||
|
- `thinkingHigh`: "高"
|
||||||
|
- `thinkingMax`: "最大"
|
||||||
|
- `thinkingContextHint`: "启用思考会占用更多上下文,请确保模型上下文长度和预留空间足够。"
|
||||||
|
- `loopControlSettings`: "循环控制"
|
||||||
|
- `maxRetriesPerStep`: "单步重试次数"
|
||||||
|
- `reservedContextSize`: "上下文预留大小"
|
||||||
|
- `backgroundSettings`: "后台任务"
|
||||||
|
- `maxRunningTasks`: "最大并发数"
|
||||||
|
- `keepAliveOnExit`: "退出时保持运行"
|
||||||
|
- `permissionRules`: "权限规则"
|
||||||
|
- `permissionDecision`: "处置"
|
||||||
|
- `permissionPattern`: "模式"
|
||||||
|
- `permissionAllow`: "允许"
|
||||||
|
- `permissionDeny`: "拒绝"
|
||||||
|
- `permissionAsk`: "询问"
|
||||||
|
- `addRule`: "+ 添加规则"
|
||||||
|
- `addCommonRules`: "添加常用规则"
|
||||||
|
- `hooks`: "生命周期钩子"
|
||||||
|
- `hookEvent`: "事件"
|
||||||
|
- `hookMatcher`: "匹配器"
|
||||||
|
- `hookCommand`: "命令"
|
||||||
|
- `hookTimeout`: "超时"
|
||||||
|
- `addHook`: "+ 添加钩子"
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- [ ] 在 Kimi Code 选项卡下进入供应商编辑页面,"模型映射"标签页下方出现"全局配置"区域。
|
||||||
|
- [ ] Pi 选项卡下不显示该全局配置区域。
|
||||||
|
- [ ] "启用思考"复选框控制 `[thinking].enabled`。
|
||||||
|
- [ ] 启用思考后,"思考等级"和"保留思考内容"可用;未启用时禁用。
|
||||||
|
- [ ] "思考等级"分段按钮写入 `[thinking].effort`。
|
||||||
|
- [ ] "保留思考内容"写入 `[thinking].keep = "all"`,未勾选时写入关值。
|
||||||
|
- [ ] `[loop_control]` 和 `[background]` 的数字/复选框正确读写。
|
||||||
|
- [ ] 权限规则列表可增删改,导出为 `[[permission.rules]]`。
|
||||||
|
- [ ] 钩子列表可增删改,导出为 `[[hooks]]`。
|
||||||
|
- [ ] 保存配置后,Kimi Code 的 `config.toml` 中对应顶层表内容正确。
|
||||||
|
- [ ] 不修改模型映射表格中已有的 `max_context_size` 手动编辑逻辑。
|
||||||
|
- [ ] 添加新模型时,`max_context_size` 默认按规则表自动填充,匹配不区分大小写,未命中规则时为 256000。
|
||||||
|
- [ ] 用户仍可在模型映射表格中手动覆盖自动填充的 `max_context_size`。
|
||||||
|
- [ ] `npm run build` 通过。
|
||||||
@@ -87,12 +87,13 @@ fn active_provider_and_model(config: &Config) -> Option<(String, String)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_active_config(config: &Config) -> Config {
|
fn build_active_config(config: &Config) -> Config {
|
||||||
let is_kimi_native = |p: &Provider| p.managed;
|
// Only the provider explicitly marked as active is written to the agent's
|
||||||
|
// native config. This ensures Kimi Code / Pi follow Pi Switch's choice
|
||||||
|
// instead of falling back to a managed/native provider.
|
||||||
let providers: IndexMap<String, Provider> = config
|
let providers: IndexMap<String, Provider> = config
|
||||||
.providers
|
.providers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, p)| is_kimi_native(p) || p.active)
|
.filter(|(_, p)| p.active)
|
||||||
.map(|(k, p)| (k.clone(), p.clone()))
|
.map(|(k, p)| (k.clone(), p.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,84 @@
|
|||||||
//! Configuration file I/O for KimiSwitch.
|
//! Configuration file I/O utilities for KimiSwitch.
|
||||||
//! Reads and writes the global config file and profile files.
|
//! Reads and writes the global config file and profile files.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::{Duration, Local};
|
||||||
|
|
||||||
|
/// Creates a backup of `path` inside a `backups/` subdirectory next to the
|
||||||
|
/// original file, then removes backup files older than `retention_days`.
|
||||||
|
pub fn backup_file(path: &Path, retention_days: i64) -> Result<PathBuf> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(PathBuf::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent = path.parent().context("path has no parent directory")?;
|
||||||
|
let backups_dir = parent.join("backups");
|
||||||
|
std::fs::create_dir_all(&backups_dir)
|
||||||
|
.with_context(|| format!("failed to create backups directory {}", backups_dir.display()))?;
|
||||||
|
|
||||||
|
let file_name = path.file_name().context("path has no file name")?;
|
||||||
|
let timestamp = Local::now().format("%Y%m%d_%H%M%S_%.3f").to_string();
|
||||||
|
let backup_name = format!("{}.bak.{}", file_name.to_string_lossy(), timestamp);
|
||||||
|
let mut backup_path = backups_dir.join(&backup_name);
|
||||||
|
|
||||||
|
if backup_path.exists() {
|
||||||
|
for n in 1..1000 {
|
||||||
|
let candidate = backups_dir.join(format!(
|
||||||
|
"{}.bak.{}.{:03}",
|
||||||
|
file_name.to_string_lossy(),
|
||||||
|
timestamp,
|
||||||
|
n
|
||||||
|
));
|
||||||
|
if !candidate.exists() {
|
||||||
|
backup_path = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::copy(path, &backup_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to back up {} to {}",
|
||||||
|
path.display(),
|
||||||
|
backup_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
cleanup_old_backups(&backups_dir, retention_days)?;
|
||||||
|
|
||||||
|
Ok(backup_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes backup files in `dir` older than `retention_days`, based on file
|
||||||
|
/// modification time. Only files whose names contain `.bak.` are deleted.
|
||||||
|
fn cleanup_old_backups(dir: &Path, retention_days: i64) -> Result<()> {
|
||||||
|
let cutoff = Local::now() - Duration::days(retention_days);
|
||||||
|
let entries = std::fs::read_dir(dir)
|
||||||
|
.with_context(|| format!("failed to read backups directory {}", dir.display()))?;
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry.context("failed to read directory entry")?;
|
||||||
|
let path = entry.path();
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
|
if !name.contains(".bak.") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let metadata = entry
|
||||||
|
.metadata()
|
||||||
|
.with_context(|| format!("failed to read metadata for {}", path.display()))?;
|
||||||
|
let modified = metadata
|
||||||
|
.modified()
|
||||||
|
.with_context(|| format!("failed to read modified time for {}", path.display()))?;
|
||||||
|
let modified: chrono::DateTime<Local> = modified.into();
|
||||||
|
if modified < cutoff {
|
||||||
|
std::fs::remove_file(&path)
|
||||||
|
.with_context(|| format!("failed to remove old backup {}", path.display()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ fn set_setting_tx(tx: &rusqlite::Transaction, key: &str, value: &str) -> DbResul
|
|||||||
fn provider_type_for_str(s: &str) -> ProviderType {
|
fn provider_type_for_str(s: &str) -> ProviderType {
|
||||||
match s {
|
match s {
|
||||||
"anthropic" => ProviderType::Anthropic,
|
"anthropic" => ProviderType::Anthropic,
|
||||||
|
"openai" => ProviderType::Openai,
|
||||||
"openai_responses" => ProviderType::OpenaiResponses,
|
"openai_responses" => ProviderType::OpenaiResponses,
|
||||||
"google-genai" => ProviderType::GoogleGenai,
|
"google-genai" => ProviderType::GoogleGenai,
|
||||||
"vertexai" => ProviderType::Vertexai,
|
"vertexai" => ProviderType::Vertexai,
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ use indexmap::IndexMap;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use toml::value::{Table, Value as TomlValue};
|
use toml::value::{Table, Value as TomlValue};
|
||||||
|
|
||||||
|
use crate::config_io::backup_file;
|
||||||
use crate::models::{Agent, Config, Model, Provider, ProviderType};
|
use crate::models::{Agent, Config, Model, Provider, ProviderType};
|
||||||
|
|
||||||
pub type KimiResult<T> = anyhow::Result<T>;
|
pub type KimiResult<T> = anyhow::Result<T>;
|
||||||
|
|
||||||
pub fn kimi_code_config_dir() -> PathBuf {
|
pub fn kimi_code_config_dir() -> PathBuf {
|
||||||
dirs::home_dir()
|
std::env::var_os("KIMI_CODE_HOME")
|
||||||
.map(|h| h.join(".kimi-code"))
|
.map(PathBuf::from)
|
||||||
.expect("failed to resolve home directory")
|
.or_else(|| dirs::home_dir().map(|h| h.join(".kimi-code")))
|
||||||
|
.expect("failed to resolve Kimi Code config directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn kimi_code_config_path() -> PathBuf {
|
pub fn kimi_code_config_path() -> PathBuf {
|
||||||
@@ -50,21 +52,9 @@ pub fn save_kimi_code_config(value: &TomlValue) -> KimiResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let timestamp = chrono::Local::now()
|
backup_file(&path, 7).with_context(|| {
|
||||||
.format("%Y%m%d_%H%M%S_%.3f")
|
format!("failed to back up Kimi Code config {}", path.display())
|
||||||
.to_string();
|
})?;
|
||||||
let mut backup_path = path.with_extension(format!("toml.bak.{}", timestamp));
|
|
||||||
if backup_path.exists() {
|
|
||||||
for n in 1..1000 {
|
|
||||||
let candidate = path.with_extension(format!("toml.bak.{}.{:03}", timestamp, n));
|
|
||||||
if !candidate.exists() {
|
|
||||||
backup_path = candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::fs::copy(&path, &backup_path)
|
|
||||||
.with_context(|| format!("failed to back up {} to {}", path.display(), backup_path.display()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = toml::to_string_pretty(value)
|
let content = toml::to_string_pretty(value)
|
||||||
@@ -101,6 +91,16 @@ pub fn kimi_code_to_config(value: &TomlValue) -> Config {
|
|||||||
|| table.get("managed").and_then(|v| v.as_bool()).unwrap_or(false);
|
|| table.get("managed").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
let enabled = table.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
|
let enabled = table.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||||
|
|
||||||
|
let env: IndexMap<String, String> = table
|
||||||
|
.get("env")
|
||||||
|
.and_then(|v| v.as_table())
|
||||||
|
.map(|t| {
|
||||||
|
t.iter()
|
||||||
|
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let raw_other = {
|
let raw_other = {
|
||||||
let mut rest = table.clone();
|
let mut rest = table.clone();
|
||||||
rest.remove("type");
|
rest.remove("type");
|
||||||
@@ -109,6 +109,7 @@ pub fn kimi_code_to_config(value: &TomlValue) -> Config {
|
|||||||
rest.remove("managed");
|
rest.remove("managed");
|
||||||
rest.remove("enabled");
|
rest.remove("enabled");
|
||||||
rest.remove("oauth");
|
rest.remove("oauth");
|
||||||
|
rest.remove("env");
|
||||||
toml_value_to_json(&TomlValue::Table(rest))
|
toml_value_to_json(&TomlValue::Table(rest))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ pub fn kimi_code_to_config(value: &TomlValue) -> Config {
|
|||||||
provider_type,
|
provider_type,
|
||||||
base_url: base_url.filter(|s| !s.is_empty()),
|
base_url: base_url.filter(|s| !s.is_empty()),
|
||||||
api_key: api_key.filter(|s| !s.is_empty()),
|
api_key: api_key.filter(|s| !s.is_empty()),
|
||||||
env: IndexMap::new(),
|
env,
|
||||||
note: None,
|
note: None,
|
||||||
official_url: None,
|
official_url: None,
|
||||||
managed,
|
managed,
|
||||||
@@ -226,13 +227,11 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
|
|||||||
.unwrap_or(TomlValue::String("".to_string())),
|
.unwrap_or(TomlValue::String("".to_string())),
|
||||||
);
|
);
|
||||||
|
|
||||||
let is_kimi_native = |p: &Provider| p.managed;
|
|
||||||
|
|
||||||
let mut providers_table = Table::new();
|
let mut providers_table = Table::new();
|
||||||
for (name, provider) in &config.providers {
|
for (name, provider) in &config.providers {
|
||||||
// Skip inactive custom providers when writing to Kimi Code config.
|
// Only write the active provider to Kimi Code config so the agent
|
||||||
// Kimi native (managed) providers are always preserved.
|
// follows Pi Switch's selection.
|
||||||
if !is_kimi_native(provider) && !provider.active {
|
if !provider.active {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut pt = Table::new();
|
let mut pt = Table::new();
|
||||||
@@ -244,6 +243,13 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
|
|||||||
pt.insert("api_key".to_string(), TomlValue::String(api_key));
|
pt.insert("api_key".to_string(), TomlValue::String(api_key));
|
||||||
}
|
}
|
||||||
pt.insert("enabled".to_string(), TomlValue::Boolean(provider.enabled));
|
pt.insert("enabled".to_string(), TomlValue::Boolean(provider.enabled));
|
||||||
|
if !provider.env.is_empty() {
|
||||||
|
let mut env_table = Table::new();
|
||||||
|
for (k, v) in &provider.env {
|
||||||
|
env_table.insert(k.clone(), TomlValue::String(v.clone()));
|
||||||
|
}
|
||||||
|
pt.insert("env".to_string(), TomlValue::Table(env_table));
|
||||||
|
}
|
||||||
if provider.managed {
|
if provider.managed {
|
||||||
// Preserve existing oauth config if present, otherwise create a default entry.
|
// Preserve existing oauth config if present, otherwise create a default entry.
|
||||||
let oauth = provider
|
let oauth = provider
|
||||||
@@ -258,9 +264,10 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
|
|||||||
});
|
});
|
||||||
pt.insert("oauth".to_string(), oauth);
|
pt.insert("oauth".to_string(), oauth);
|
||||||
}
|
}
|
||||||
// Merge remaining raw fields (oauth is handled explicitly above).
|
// Merge remaining raw fields (oauth and env are handled explicitly above).
|
||||||
if let TomlValue::Table(mut extra) = json_to_toml(&provider.raw_other).unwrap_or(TomlValue::Table(Table::new())) {
|
if let TomlValue::Table(mut extra) = json_to_toml(&provider.raw_other).unwrap_or(TomlValue::Table(Table::new())) {
|
||||||
extra.remove("oauth");
|
extra.remove("oauth");
|
||||||
|
extra.remove("env");
|
||||||
for (k, v) in extra {
|
for (k, v) in extra {
|
||||||
pt.insert(k, v);
|
pt.insert(k, v);
|
||||||
}
|
}
|
||||||
@@ -273,7 +280,7 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
|
|||||||
let active_provider_names: std::collections::HashSet<&str> = config
|
let active_provider_names: std::collections::HashSet<&str> = config
|
||||||
.providers
|
.providers
|
||||||
.values()
|
.values()
|
||||||
.filter(|p| is_kimi_native(p) || p.active)
|
.filter(|p| p.active)
|
||||||
.map(|p| p.name.as_str())
|
.map(|p| p.name.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod config_io;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod kimi_code_io;
|
pub mod kimi_code_io;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
|
|||||||
+11
-33
@@ -11,15 +11,17 @@ use indexmap::IndexMap;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::config_io::backup_file;
|
||||||
use crate::models::{Config, Model, Provider, ProviderType};
|
use crate::models::{Config, Model, Provider, ProviderType};
|
||||||
|
|
||||||
pub type PiResult<T> = anyhow::Result<T>;
|
pub type PiResult<T> = anyhow::Result<T>;
|
||||||
|
|
||||||
/// Returns the default Pi agent config directory for the current user.
|
/// Returns the default Pi agent config directory for the current user.
|
||||||
pub fn pi_agent_dir() -> PathBuf {
|
pub fn pi_agent_dir() -> PathBuf {
|
||||||
dirs::home_dir()
|
std::env::var_os("PI_CODING_AGENT_DIR")
|
||||||
.map(|h| h.join(".pi").join("agent"))
|
.map(PathBuf::from)
|
||||||
.expect("failed to resolve home directory")
|
.or_else(|| dirs::home_dir().map(|h| h.join(".pi").join("agent")))
|
||||||
|
.expect("failed to resolve Pi agent config directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to Pi's `models.json` file.
|
/// Returns the path to Pi's `models.json` file.
|
||||||
@@ -131,21 +133,9 @@ pub fn save_pi_models(file: &PiModelsFile) -> PiResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let timestamp = chrono::Local::now()
|
backup_file(&path, 7).with_context(|| {
|
||||||
.format("%Y%m%d_%H%M%S_%.3f")
|
format!("failed to back up Pi models {}", path.display())
|
||||||
.to_string();
|
})?;
|
||||||
let mut backup_path = path.with_extension(format!("json.bak.{}", timestamp));
|
|
||||||
if backup_path.exists() {
|
|
||||||
for n in 1..1000 {
|
|
||||||
let candidate = path.with_extension(format!("json.bak.{}.{:03}", timestamp, n));
|
|
||||||
if !candidate.exists() {
|
|
||||||
backup_path = candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::fs::copy(&path, &backup_path)
|
|
||||||
.with_context(|| format!("failed to back up {} to {}", path.display(), backup_path.display()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = serde_json::to_string_pretty(file)
|
let content = serde_json::to_string_pretty(file)
|
||||||
@@ -195,21 +185,9 @@ pub fn save_pi_settings(file: &PiSettingsFile) -> PiResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let timestamp = chrono::Local::now()
|
backup_file(&path, 7).with_context(|| {
|
||||||
.format("%Y%m%d_%H%M%S_%.3f")
|
format!("failed to back up Pi settings {}", path.display())
|
||||||
.to_string();
|
})?;
|
||||||
let mut backup_path = path.with_extension(format!("json.bak.{}", timestamp));
|
|
||||||
if backup_path.exists() {
|
|
||||||
for n in 1..1000 {
|
|
||||||
let candidate = path.with_extension(format!("json.bak.{}.{:03}", timestamp, n));
|
|
||||||
if !candidate.exists() {
|
|
||||||
backup_path = candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::fs::copy(&path, &backup_path)
|
|
||||||
.with_context(|| format!("failed to back up {} to {}", path.display(), backup_path.display()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = serde_json::to_string_pretty(file)
|
let content = serde_json::to_string_pretty(file)
|
||||||
|
|||||||
+91
-19
@@ -5,10 +5,35 @@ import { useConfig } from "./hooks/useConfig";
|
|||||||
import { ProviderList } from "./components/ProviderList";
|
import { ProviderList } from "./components/ProviderList";
|
||||||
import { ProviderEdit } from "./components/ProviderEdit";
|
import { ProviderEdit } from "./components/ProviderEdit";
|
||||||
import { useTranslation } from "./i18n";
|
import { useTranslation } from "./i18n";
|
||||||
|
import { getDefaultMaxContextSize } from "./lib/model-defaults";
|
||||||
import type { Agent, Model, Provider } from "./types";
|
import type { Agent, Model, Provider } from "./types";
|
||||||
|
|
||||||
const AGENT_STORAGE_KEY = "pi-switch-agent";
|
const AGENT_STORAGE_KEY = "pi-switch-agent";
|
||||||
|
|
||||||
|
function getProviderDefaultModel(provider: Provider): string | null {
|
||||||
|
const raw = provider.raw_other as Record<string, unknown> | undefined;
|
||||||
|
const value = raw?.default_model;
|
||||||
|
return typeof value === "string" ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProviderDefaultModel(provider: Provider, alias: string | null): Provider {
|
||||||
|
const raw = (provider.raw_other as Record<string, unknown> | undefined) ?? {};
|
||||||
|
if (alias) {
|
||||||
|
return { ...provider, raw_other: { ...raw, default_model: alias } };
|
||||||
|
}
|
||||||
|
const { default_model: _, ...rest } = raw;
|
||||||
|
return { ...provider, raw_other: rest };
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFirstModelForProvider(models: Record<string, Model>, providerName: string): string | null {
|
||||||
|
for (const [key, m] of Object.entries(models)) {
|
||||||
|
if (m.provider === providerName) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const AGENTS: { key: Agent; label: string }[] = [
|
const AGENTS: { key: Agent; label: string }[] = [
|
||||||
{ key: "kimi_code", label: "Kimi Code" },
|
{ key: "kimi_code", label: "Kimi Code" },
|
||||||
{ key: "pi", label: "Pi" },
|
{ key: "pi", label: "Pi" },
|
||||||
@@ -188,32 +213,61 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSetDefaultModel = (alias: string) => {
|
const handleSetDefaultModel = (alias: string) => {
|
||||||
updateConfig((cfg) => ({ ...cfg, default_model: alias }));
|
updateConfig((cfg) => {
|
||||||
|
const model = cfg.models[alias];
|
||||||
|
if (!model) return cfg;
|
||||||
|
const provider = cfg.providers[model.provider];
|
||||||
|
if (!provider) return cfg;
|
||||||
|
const providers = {
|
||||||
|
...cfg.providers,
|
||||||
|
[provider.name]: setProviderDefaultModel(provider, alias),
|
||||||
|
};
|
||||||
|
return { ...cfg, providers, default_model: alias };
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSwitchProvider = async (name: string) => {
|
const handleSwitchProvider = async (name: string) => {
|
||||||
updateConfig((cfg) => {
|
updateConfig((cfg) => {
|
||||||
const target = cfg.providers[name];
|
const target = cfg.providers[name];
|
||||||
if (!target || target.managed) return cfg;
|
if (!target) return cfg;
|
||||||
|
|
||||||
const isKimiNative = (p: Provider) => p.managed === true;
|
// Find the currently active provider so we can remember its default
|
||||||
|
// model before switching away.
|
||||||
// Mark the selected provider as active and deactivate other custom providers.
|
let activeProviderName: string | null = null;
|
||||||
// Kimi native providers are left untouched. No provider/model records are deleted.
|
|
||||||
const providers: Record<string, Provider> = {};
|
|
||||||
for (const [key, p] of Object.entries(cfg.providers)) {
|
for (const [key, p] of Object.entries(cfg.providers)) {
|
||||||
providers[key] = { ...p, active: isKimiNative(p) ? undefined : key === name };
|
if (p.active) {
|
||||||
}
|
activeProviderName = key;
|
||||||
|
|
||||||
// Set default model to the first model of the selected provider.
|
|
||||||
let default_model: string | null = null;
|
|
||||||
for (const [key, m] of Object.entries(cfg.models)) {
|
|
||||||
if (m.provider === name) {
|
|
||||||
default_model = key;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only the selected provider is active. All other providers (including
|
||||||
|
// Kimi native/managed and custom) are deactivated so the target agent
|
||||||
|
// follows Pi Switch's choice exactly.
|
||||||
|
const providers: Record<string, Provider> = {};
|
||||||
|
for (const [key, p] of Object.entries(cfg.providers)) {
|
||||||
|
providers[key] = { ...p, active: key === name };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember the active provider's current default model (if it belongs to
|
||||||
|
// that provider) so switching back later restores the user's choice.
|
||||||
|
if (activeProviderName && cfg.default_model) {
|
||||||
|
const activeProvider = providers[activeProviderName];
|
||||||
|
if (activeProvider && cfg.models[cfg.default_model]?.provider === activeProviderName) {
|
||||||
|
providers[activeProviderName] = setProviderDefaultModel(
|
||||||
|
activeProvider,
|
||||||
|
cfg.default_model
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the target provider's remembered default model; fall back to
|
||||||
|
// its first model when there is no saved preference or it is invalid.
|
||||||
|
let default_model = getProviderDefaultModel(target);
|
||||||
|
if (!default_model || cfg.models[default_model]?.provider !== name) {
|
||||||
|
default_model = findFirstModelForProvider(cfg.models, name);
|
||||||
|
}
|
||||||
|
|
||||||
return { ...cfg, providers, default_model };
|
return { ...cfg, providers, default_model };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -246,7 +300,12 @@ export default function App() {
|
|||||||
alias = `${m.alias}-${n}`;
|
alias = `${m.alias}-${n}`;
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
updatedModels[alias] = { ...m, alias, provider: provider.name };
|
updatedModels[alias] = {
|
||||||
|
...m,
|
||||||
|
alias,
|
||||||
|
provider: provider.name,
|
||||||
|
max_context_size: m.max_context_size ?? getDefaultMaxContextSize(m.model),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let default_model = cfg.default_model;
|
let default_model = cfg.default_model;
|
||||||
@@ -254,6 +313,13 @@ export default function App() {
|
|||||||
default_model = null;
|
default_model = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remember this provider's default model preference so it survives
|
||||||
|
// provider switches.
|
||||||
|
const updatedProvider = providers[provider.name];
|
||||||
|
if (updatedProvider && default_model && updatedModels[default_model]?.provider === provider.name) {
|
||||||
|
providers[provider.name] = setProviderDefaultModel(updatedProvider, default_model);
|
||||||
|
}
|
||||||
|
|
||||||
return { ...cfg, providers, models: updatedModels, default_model };
|
return { ...cfg, providers, models: updatedModels, default_model };
|
||||||
});
|
});
|
||||||
if (provider.name !== editingProvider) {
|
if (provider.name !== editingProvider) {
|
||||||
@@ -348,6 +414,10 @@ export default function App() {
|
|||||||
(m) => m.provider === currentProvider.name
|
(m) => m.provider === currentProvider.name
|
||||||
)}
|
)}
|
||||||
defaultModel={config.default_model}
|
defaultModel={config.default_model}
|
||||||
|
rawOther={config.raw_other}
|
||||||
|
onRawOtherChange={(nextRawOther) =>
|
||||||
|
updateConfig((cfg) => ({ ...cfg, raw_other: nextRawOther }))
|
||||||
|
}
|
||||||
onBack={() => setView("list")}
|
onBack={() => setView("list")}
|
||||||
onChange={handleUpdateProvider}
|
onChange={handleUpdateProvider}
|
||||||
onDelete={() => handleDeleteProvider(currentProvider.name)}
|
onDelete={() => handleDeleteProvider(currentProvider.name)}
|
||||||
@@ -385,7 +455,9 @@ export default function App() {
|
|||||||
const providerModels = Object.values(config.models).filter(
|
const providerModels = Object.values(config.models).filter(
|
||||||
(m) => m.provider === currentProvider.name
|
(m) => m.provider === currentProvider.name
|
||||||
);
|
);
|
||||||
const alias = `${currentProvider.name}-${providerModels.length + 1}`;
|
const roles = ["Sonnet", "Opus", "Fable", "Haiku"];
|
||||||
|
const defaultRole = roles[providerModels.length % roles.length];
|
||||||
|
const alias = `新模型[${currentProvider.name}]`;
|
||||||
updateConfig((cfg) => ({
|
updateConfig((cfg) => ({
|
||||||
...cfg,
|
...cfg,
|
||||||
models: {
|
models: {
|
||||||
@@ -394,9 +466,9 @@ export default function App() {
|
|||||||
alias,
|
alias,
|
||||||
provider: currentProvider.name,
|
provider: currentProvider.name,
|
||||||
model: "",
|
model: "",
|
||||||
max_context_size: 128000,
|
max_context_size: getDefaultMaxContextSize(alias),
|
||||||
display_name: null,
|
display_name: null,
|
||||||
role: null,
|
role: defaultRole,
|
||||||
supports_1m: false,
|
supports_1m: false,
|
||||||
capabilities: [],
|
capabilities: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useTranslation } from "../i18n";
|
||||||
|
import type { TranslationKey } from "../i18n/zh";
|
||||||
|
import { getAgentSettings, setAgentSettings } from "../lib/agent-settings";
|
||||||
|
import type { AgentSettings, Hook, PermissionRule } from "../types";
|
||||||
|
|
||||||
|
interface AgentSettingsPanelProps {
|
||||||
|
rawOther: unknown;
|
||||||
|
onChange: (nextRawOther: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const THINKING_LEVELS = ["low", "medium", "high", "max"] as const;
|
||||||
|
const THINKING_LABELS: Record<(typeof THINKING_LEVELS)[number], TranslationKey> = {
|
||||||
|
low: "thinkingLow",
|
||||||
|
medium: "thinkingMedium",
|
||||||
|
high: "thinkingHigh",
|
||||||
|
max: "thinkingMax",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PERMISSION_DECISIONS = ["allow", "deny", "ask"] as const;
|
||||||
|
const PERMISSION_LABELS: Record<(typeof PERMISSION_DECISIONS)[number], TranslationKey> = {
|
||||||
|
allow: "permissionAllow",
|
||||||
|
deny: "permissionDeny",
|
||||||
|
ask: "permissionAsk",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMMON_EVENTS = ["PreToolUse", "PostToolUse"] as const;
|
||||||
|
|
||||||
|
export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const settings = getAgentSettings(rawOther);
|
||||||
|
|
||||||
|
const update = (patch: Partial<AgentSettings>) => {
|
||||||
|
onChange(setAgentSettings(rawOther, patch));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateThinking = (patch: Partial<AgentSettings["thinking"]>) => {
|
||||||
|
update({ thinking: { ...settings.thinking, ...patch } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLoopControl = (patch: Partial<AgentSettings["loop_control"]>) => {
|
||||||
|
update({ loop_control: { ...settings.loop_control, ...patch } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBackground = (patch: Partial<AgentSettings["background"]>) => {
|
||||||
|
update({ background: { ...settings.background, ...patch } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setRules = (rules: PermissionRule[]) => {
|
||||||
|
update({ permission: { rules } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setHooks = (hooks: Hook[]) => {
|
||||||
|
update({ hooks });
|
||||||
|
};
|
||||||
|
|
||||||
|
const thinkingEnabled = settings.thinking?.enabled ?? true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
<h3 className="text-gray-400 text-sm font-medium">{t("agentSettings")}</h3>
|
||||||
|
|
||||||
|
<Card title={t("enableThinking")}>
|
||||||
|
<Checkbox
|
||||||
|
label={t("enableThinking")}
|
||||||
|
checked={thinkingEnabled}
|
||||||
|
onChange={(checked) => updateThinking({ enabled: checked })}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<span className="text-sm text-gray-400">{t("thinkingLevel")}</span>
|
||||||
|
<Segmented
|
||||||
|
options={THINKING_LEVELS.map((lvl) => ({
|
||||||
|
key: lvl,
|
||||||
|
label: t(THINKING_LABELS[lvl]),
|
||||||
|
}))}
|
||||||
|
value={settings.thinking?.effort ?? "medium"}
|
||||||
|
onChange={(effort) =>
|
||||||
|
updateThinking({ effort: effort as NonNullable<AgentSettings["thinking"]>["effort"] })
|
||||||
|
}
|
||||||
|
disabled={!thinkingEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
label={t("thinkingKeep")}
|
||||||
|
checked={settings.thinking?.keep === "all"}
|
||||||
|
disabled={!thinkingEnabled}
|
||||||
|
onChange={(checked) => updateThinking({ keep: checked ? "all" : false })}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">{t("thinkingContextHint")}</p>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={t("loopControlSettings")}>
|
||||||
|
<NumberField
|
||||||
|
label={t("maxRetriesPerStep")}
|
||||||
|
value={settings.loop_control?.max_retries_per_step ?? 3}
|
||||||
|
onChange={(v) => updateLoopControl({ max_retries_per_step: v })}
|
||||||
|
/>
|
||||||
|
<NumberField
|
||||||
|
label={t("reservedContextSize")}
|
||||||
|
value={settings.loop_control?.reserved_context_size ?? 50000}
|
||||||
|
onChange={(v) => updateLoopControl({ reserved_context_size: v })}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={t("backgroundSettings")}>
|
||||||
|
<NumberField
|
||||||
|
label={t("maxRunningTasks")}
|
||||||
|
value={settings.background?.max_running_tasks}
|
||||||
|
onChange={(v) => updateBackground({ max_running_tasks: v })}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t("keepAliveOnExit")}
|
||||||
|
checked={settings.background?.keep_alive_on_exit ?? false}
|
||||||
|
onChange={(checked) => updateBackground({ keep_alive_on_exit: checked })}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={t("permissionRules")}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(settings.permission?.rules ?? []).map((rule, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2">
|
||||||
|
<select
|
||||||
|
className="bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={rule.decision ?? "allow"}
|
||||||
|
onChange={(e) => {
|
||||||
|
const rules = [...(settings.permission?.rules ?? [])];
|
||||||
|
rules[idx] = {
|
||||||
|
...rule,
|
||||||
|
decision: e.target.value as PermissionRule["decision"],
|
||||||
|
};
|
||||||
|
setRules(rules);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{PERMISSION_DECISIONS.map((d) => (
|
||||||
|
<option key={d} value={d}>
|
||||||
|
{t(PERMISSION_LABELS[d])}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 min-w-0 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={rule.pattern ?? ""}
|
||||||
|
placeholder={t("permissionPattern")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const rules = [...(settings.permission?.rules ?? [])];
|
||||||
|
rules[idx] = { ...rule, pattern: e.target.value };
|
||||||
|
setRules(rules);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const rules = [...(settings.permission?.rules ?? [])];
|
||||||
|
rules.splice(idx, 1);
|
||||||
|
setRules(rules);
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 text-sm text-red-400 hover:bg-red-900/20 rounded"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setRules([
|
||||||
|
...(settings.permission?.rules ?? []),
|
||||||
|
{ decision: "allow", pattern: "" },
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
className="px-3 py-1.5 text-sm border border-[#2a2a2e] rounded hover:bg-[#252529]"
|
||||||
|
>
|
||||||
|
{t("addRule")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setRules([
|
||||||
|
...(settings.permission?.rules ?? []),
|
||||||
|
{ decision: "allow", pattern: "Read" },
|
||||||
|
{ decision: "deny", pattern: "Bash(rm -rf*)" },
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
className="px-3 py-1.5 text-sm border border-[#2a2a2e] rounded hover:bg-[#252529]"
|
||||||
|
>
|
||||||
|
{t("addCommonRules")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={t("hooks")}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(settings.hooks ?? []).map((hook, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2 flex-wrap">
|
||||||
|
<input
|
||||||
|
list="hook-events"
|
||||||
|
type="text"
|
||||||
|
className="w-32 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={hook.event ?? ""}
|
||||||
|
placeholder={t("hookEvent")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const hooks = [...(settings.hooks ?? [])];
|
||||||
|
hooks[idx] = { ...hook, event: e.target.value };
|
||||||
|
setHooks(hooks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<datalist id="hook-events">
|
||||||
|
{COMMON_EVENTS.map((e) => (
|
||||||
|
<option key={e} value={e} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-24 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={hook.matcher ?? ""}
|
||||||
|
placeholder={t("hookMatcher")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const hooks = [...(settings.hooks ?? [])];
|
||||||
|
hooks[idx] = { ...hook, matcher: e.target.value };
|
||||||
|
setHooks(hooks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 min-w-0 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={hook.command ?? ""}
|
||||||
|
placeholder={t("hookCommand")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const hooks = [...(settings.hooks ?? [])];
|
||||||
|
hooks[idx] = { ...hook, command: e.target.value };
|
||||||
|
setHooks(hooks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="w-20 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm"
|
||||||
|
value={hook.timeout ?? ""}
|
||||||
|
placeholder={t("hookTimeout")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const hooks = [...(settings.hooks ?? [])];
|
||||||
|
hooks[idx] = {
|
||||||
|
...hook,
|
||||||
|
timeout: e.target.value === "" ? undefined : Number(e.target.value),
|
||||||
|
};
|
||||||
|
setHooks(hooks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const hooks = [...(settings.hooks ?? [])];
|
||||||
|
hooks.splice(idx, 1);
|
||||||
|
setHooks(hooks);
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 text-sm text-red-400 hover:bg-red-900/20 rounded"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setHooks([...(settings.hooks ?? []), { event: "", matcher: "", command: "" }]);
|
||||||
|
}}
|
||||||
|
className="px-3 py-1.5 text-sm border border-[#2a2a2e] rounded hover:bg-[#252529]"
|
||||||
|
>
|
||||||
|
{t("addHook")}
|
||||||
|
</button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Card({ title, children }: { title: string; children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#16161a] border border-[#2a2a2e] rounded-xl p-4 space-y-3">
|
||||||
|
<h4 className="text-gray-400 text-sm font-medium">{title}</h4>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
label,
|
||||||
|
checked,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
checked: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={`flex items-center gap-2 text-sm ${
|
||||||
|
disabled ? "text-gray-600" : "text-[#e5e5e7]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
|
className="rounded border-[#2a2a2e] bg-[#1f1f23] text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumberField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value?: number;
|
||||||
|
onChange: (value: number | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="flex items-center gap-3 text-sm text-[#e5e5e7]">
|
||||||
|
<span className="text-gray-400">{label}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value;
|
||||||
|
onChange(raw === "" ? undefined : Number(raw));
|
||||||
|
}}
|
||||||
|
className="w-32 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Segmented({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
options: { key: string; label: string }[];
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center rounded overflow-hidden border border-[#2a2a2e] ${
|
||||||
|
disabled ? "opacity-50" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.key}
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => onChange(opt.key)}
|
||||||
|
className={`px-3 py-1 text-sm ${
|
||||||
|
value === opt.key
|
||||||
|
? "bg-blue-600 text-white"
|
||||||
|
: "bg-[#1f1f23] text-gray-400 hover:bg-[#252529]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useId, useState } from "react";
|
import { useEffect, useId, useState } from "react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useTranslation } from "../i18n";
|
import { useTranslation } from "../i18n";
|
||||||
|
import { getDefaultMaxContextSize } from "../lib/model-defaults";
|
||||||
|
import { AgentSettingsPanel } from "./AgentSettingsPanel";
|
||||||
import type { Agent, DiscoveredModel, Model, Provider, ProviderType } from "../types";
|
import type { Agent, DiscoveredModel, Model, Provider, ProviderType } from "../types";
|
||||||
|
|
||||||
const PROVIDER_TYPES: ProviderType[] = [
|
const PROVIDER_TYPES: ProviderType[] = [
|
||||||
@@ -47,6 +49,8 @@ interface ProviderEditProps {
|
|||||||
provider: Provider;
|
provider: Provider;
|
||||||
models: Model[];
|
models: Model[];
|
||||||
defaultModel: string | null;
|
defaultModel: string | null;
|
||||||
|
rawOther: unknown;
|
||||||
|
onRawOtherChange: (nextRawOther: unknown) => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onChange: (provider: Provider) => void;
|
onChange: (provider: Provider) => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
@@ -64,6 +68,8 @@ export function ProviderEdit({
|
|||||||
provider,
|
provider,
|
||||||
models,
|
models,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
|
rawOther,
|
||||||
|
onRawOtherChange,
|
||||||
onBack,
|
onBack,
|
||||||
onChange,
|
onChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -281,6 +287,7 @@ export function ProviderEdit({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "models" && (
|
{activeTab === "models" && (
|
||||||
|
<>
|
||||||
<ModelMapping
|
<ModelMapping
|
||||||
agent={agent}
|
agent={agent}
|
||||||
provider={provider}
|
provider={provider}
|
||||||
@@ -292,6 +299,13 @@ export function ProviderEdit({
|
|||||||
onBulkAdd={onBulkAdd}
|
onBulkAdd={onBulkAdd}
|
||||||
onSetDefault={onSetDefault}
|
onSetDefault={onSetDefault}
|
||||||
/>
|
/>
|
||||||
|
{agent === "kimi_code" && (
|
||||||
|
<AgentSettingsPanel
|
||||||
|
rawOther={rawOther}
|
||||||
|
onChange={onRawOtherChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "json" && (
|
{activeTab === "json" && (
|
||||||
@@ -365,14 +379,16 @@ function ModelMapping({
|
|||||||
for (const dm of discovered) {
|
for (const dm of discovered) {
|
||||||
if (!selected.has(dm.id)) continue;
|
if (!selected.has(dm.id)) continue;
|
||||||
const alias = dm.id.replace(/[^a-zA-Z0-9_-]/g, "-");
|
const alias = dm.id.replace(/[^a-zA-Z0-9_-]/g, "-");
|
||||||
|
const role = `${dm.id}[${provider.name}]`;
|
||||||
|
const max_context_size = dm.max_context_size ?? getDefaultMaxContextSize(dm.id);
|
||||||
toAdd.push({
|
toAdd.push({
|
||||||
alias,
|
alias,
|
||||||
provider: provider.name,
|
provider: provider.name,
|
||||||
model: dm.id,
|
model: dm.id,
|
||||||
max_context_size: dm.max_context_size ?? 128000,
|
max_context_size,
|
||||||
display_name: dm.display_name,
|
display_name: dm.display_name,
|
||||||
role: null,
|
role,
|
||||||
supports_1m: (dm.max_context_size ?? 0) >= 1_000_000,
|
supports_1m: max_context_size >= 1_000_000,
|
||||||
capabilities: [],
|
capabilities: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -467,29 +483,21 @@ function ModelMapping({
|
|||||||
{models.map((m) => (
|
{models.map((m) => (
|
||||||
<tr key={m.alias} className="hover:bg-[#1c1c20]">
|
<tr key={m.alias} className="hover:bg-[#1c1c20]">
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<input
|
<select
|
||||||
className="w-full bg-transparent border border-[#2a2a2e] rounded px-2 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
className="w-full bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
value={m.role || ""}
|
value={m.role || "Sonnet"}
|
||||||
placeholder={m.alias}
|
onChange={(e) => onModelChange({ ...m, role: e.target.value })}
|
||||||
onChange={(e) =>
|
>
|
||||||
onModelChange({
|
<option value="Sonnet">Sonnet</option>
|
||||||
...m,
|
<option value="Opus">Opus</option>
|
||||||
role: e.target.value || null,
|
<option value="Fable">Fable</option>
|
||||||
})
|
<option value="Haiku">Haiku</option>
|
||||||
}
|
</select>
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<input
|
<span className="text-sm text-gray-400">
|
||||||
className="w-full bg-transparent border border-[#2a2a2e] rounded px-2 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
{m.model ? `${m.model}[${m.provider}]` : m.alias}
|
||||||
value={m.display_name || ""}
|
</span>
|
||||||
onChange={(e) =>
|
|
||||||
onModelChange({
|
|
||||||
...m,
|
|
||||||
display_name: e.target.value || null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -92,10 +92,9 @@ export function ProviderList({
|
|||||||
(m) => m.provider === provider.name
|
(m) => m.provider === provider.name
|
||||||
);
|
);
|
||||||
const defaultModelName = defaultModel
|
const defaultModelName = defaultModel
|
||||||
? models[defaultModel]?.display_name || defaultModel
|
? models[defaultModel]?.model || models[defaultModel]?.display_name || defaultModel
|
||||||
: null;
|
: null;
|
||||||
const isKimiNative = provider.managed === true;
|
const isActive = provider.active === true;
|
||||||
const isActive = !isKimiNative && provider.active === true;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user