From e1937233eca796c75747dfb2b0ad0d3bc4af42b0 Mon Sep 17 00:00:00 2001 From: leookun Date: Wed, 26 Aug 2026 15:46:11 +0800 Subject: [PATCH] merge old config --- Cargo.lock | 20 + apps/desktop/package-lock.json | 15 + apps/desktop/package.json | 2 + apps/desktop/src/App.tsx | 2 - apps/desktop/src/api.ts | 131 +- .../src/components/ProviderEditor.module.scss | 15 - .../desktop/src/components/ProviderEditor.tsx | 30 - .../src/components/ProviderTable.module.scss | 14 - apps/desktop/src/components/ProviderTable.tsx | 31 - .../src/components/cursor/CursorGates.tsx | 9 +- .../components/cursor/CursorModelCards.tsx | 114 ++ .../components/cursor/CursorModelEditor.tsx | 197 +- .../cursor/CursorModelTestResult.module.scss | 64 + .../cursor/CursorModelTestResult.tsx | 13 +- .../cursor/CursorSettings.module.scss | 182 +- .../cursor/TabSettingsCard.module.scss | 51 - .../src/components/cursor/TabSettingsCard.tsx | 53 - .../models/LegacyModelImport.module.scss | 92 + .../components/models/LegacyModelImport.tsx | 92 + .../overview/OverviewTimeRangeFilter.tsx | 6 +- .../settings/TabSettingsCard.module.scss | 78 + .../components/settings/TabSettingsCard.tsx | 90 + apps/desktop/src/components/ui/Card.tsx | 9 +- .../src/components/ui/ConfirmDialog.tsx | 33 + .../src/components/ui/Modal.module.scss | 6 + apps/desktop/src/components/ui/Modal.tsx | 78 +- apps/desktop/src/components/ui/Tooltip.tsx | 3 +- .../src/components/ui/TooltipTrigger.tsx | 37 +- apps/desktop/src/components/ui/icons.ts | 1 + apps/desktop/src/i18n/generated/catalog.json | 1771 ++++++++--------- apps/desktop/src/i18n/locales/en-US.json | 107 +- apps/desktop/src/i18n/locales/zh-CN.json | 107 +- apps/desktop/src/layouts/AppLayout.tsx | 52 +- apps/desktop/src/pages/CursorSettingsPage.tsx | 291 ++- apps/desktop/src/pages/HomePage.tsx | 18 +- .../src/pages/ProvidersPage.module.scss | 3 - apps/desktop/src/pages/ProvidersPage.tsx | 101 - .../src/pages/SettingsPage.module.scss | 23 + apps/desktop/src/pages/SettingsPage.tsx | 66 +- apps/desktop/src/store/appStore.ts | 126 +- .../{providerDefaults.ts => modelDefaults.ts} | 0 server/Cargo.toml | 1 + .../0004_flatten_model_configuration.sql | 197 ++ server/src/config.rs | 10 + server/src/control/cursor_models.rs | 72 - server/src/control/mod.rs | 37 +- server/src/control/models.rs | 50 +- server/src/control/overview.rs | 8 +- server/src/control/providers.rs | 42 - server/src/control/service.rs | 266 +-- server/src/cursor/handlers.rs | 2 +- server/src/cursor/model_catalog.rs | 102 +- server/src/cursor/request/prepare.rs | 8 +- server/src/harness/mod.rs | 4 +- server/src/model/configuration.rs | 508 +++++ server/src/model/mod.rs | 4 +- server/src/model/provider.rs | 341 ---- server/src/provider/router.rs | 38 +- server/src/store/legacy_config.rs | 390 ++++ server/src/store/llm_calls.rs | 58 +- server/src/store/mod.rs | 3 +- server/src/store/models.rs | 416 ++++ server/src/store/overview.rs | 30 +- server/src/store/providers.rs | 1009 ---------- server/src/store/storage.rs | 33 +- server/tests/compaction.rs | 54 +- ...ider_console.rs => model_configuration.rs} | 180 +- server/tests/observability.rs | 58 +- server/tests/schema_upgrade.rs | 171 ++ server/tests/text_turn.rs | 54 +- 70 files changed, 4515 insertions(+), 3764 deletions(-) delete mode 100644 apps/desktop/src/components/ProviderEditor.module.scss delete mode 100644 apps/desktop/src/components/ProviderEditor.tsx delete mode 100644 apps/desktop/src/components/ProviderTable.module.scss delete mode 100644 apps/desktop/src/components/ProviderTable.tsx create mode 100644 apps/desktop/src/components/cursor/CursorModelCards.tsx create mode 100644 apps/desktop/src/components/cursor/CursorModelTestResult.module.scss delete mode 100644 apps/desktop/src/components/cursor/TabSettingsCard.module.scss delete mode 100644 apps/desktop/src/components/cursor/TabSettingsCard.tsx create mode 100644 apps/desktop/src/components/models/LegacyModelImport.module.scss create mode 100644 apps/desktop/src/components/models/LegacyModelImport.tsx create mode 100644 apps/desktop/src/components/settings/TabSettingsCard.module.scss create mode 100644 apps/desktop/src/components/settings/TabSettingsCard.tsx create mode 100644 apps/desktop/src/components/ui/ConfirmDialog.tsx delete mode 100644 apps/desktop/src/pages/ProvidersPage.module.scss delete mode 100644 apps/desktop/src/pages/ProvidersPage.tsx rename apps/desktop/src/utils/{providerDefaults.ts => modelDefaults.ts} (100%) create mode 100644 server/migrations/0004_flatten_model_configuration.sql delete mode 100644 server/src/control/cursor_models.rs delete mode 100644 server/src/control/providers.rs create mode 100644 server/src/model/configuration.rs delete mode 100644 server/src/model/provider.rs create mode 100644 server/src/store/legacy_config.rs create mode 100644 server/src/store/models.rs delete mode 100644 server/src/store/providers.rs rename server/tests/{provider_console.rs => model_configuration.rs} (52%) diff --git a/Cargo.lock b/Cargo.lock index 50767eb..fe21d60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1183,6 +1183,7 @@ dependencies = [ "semble-core", "serde", "serde_json", + "serde_yaml", "sha1 0.10.7", "sha2", "similar", @@ -5661,6 +5662,19 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serialize-to-javascript" version = "0.1.2" @@ -7487,6 +7501,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json index ac90bed..db961db 100644 --- a/apps/desktop/package-lock.json +++ b/apps/desktop/package-lock.json @@ -27,6 +27,7 @@ "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.8", "react-router-dom": "^7.18.2", + "sortablejs": "^1.15.7", "zrender": "^6.1.0" }, "devDependencies": { @@ -37,6 +38,7 @@ "@types/node": "^26.1.2", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", + "@types/sortablejs": "^1.15.9", "@vitejs/plugin-react": "^6.0.5", "code-inspector-plugin": "^1.6.6", "concurrently": "^9.2.1", @@ -1293,6 +1295,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/sortablejs": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -3185,6 +3194,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sortablejs": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index db78f10..d3bf081 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -37,6 +37,7 @@ "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.8", "react-router-dom": "^7.18.2", + "sortablejs": "^1.15.7", "zrender": "^6.1.0" }, "devDependencies": { @@ -47,6 +48,7 @@ "@types/node": "^26.1.2", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", + "@types/sortablejs": "^1.15.9", "@vitejs/plugin-react": "^6.0.5", "code-inspector-plugin": "^1.6.6", "concurrently": "^9.2.1", diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 3f39fec..7ec69d3 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -8,7 +8,6 @@ import { CallsPage } from "./pages/CallsPage"; import { CallDetailsPage } from "./pages/CallDetailsPage"; import { CursorSettingsPage } from "./pages/CursorSettingsPage"; import { HomePage } from "./pages/HomePage"; -import { ProvidersPage } from "./pages/ProvidersPage"; import { SettingsPage } from "./pages/SettingsPage"; import { useAppStore } from "./store/appStore"; import { updateStore } from "./store/updateStore"; @@ -22,7 +21,6 @@ export function App() { }> }> } /> - } /> } /> } /> } /> diff --git a/apps/desktop/src/api.ts b/apps/desktop/src/api.ts index 0e2d15d..c29562a 100644 --- a/apps/desktop/src/api.ts +++ b/apps/desktop/src/api.ts @@ -1,60 +1,87 @@ import type { AdRuntime } from "./components/ads/types"; import type { Locale } from "./i18n/runtime"; -export type ProviderType = "openai-chat" | "openai-responses" | "anthropic"; - -export interface Provider { - provider_id: number; - name: string; - provider_type: ProviderType; - base_url: string; - api_key?: string; - has_api_key: boolean; - custom_headers: Record; - extra_params: Record; - created_at_ms: number; - updated_at_ms: number; -} - -export interface ProviderInput { - name: string; - provider_type: ProviderType; - base_url: string; - api_key?: string; - custom_headers: Record; - extra_params: Record; -} +export type ModelType = "openai" | "anthropic"; export interface Model { model_hash: string; - provider_id: number; - model_id: string; - display_name: string; - endpoint_type: ProviderType; - request_url: string; - enabled: boolean; sort_order: number; - context_window_tokens: number | null; - max_output_tokens: number | null; - reasoning_enabled: boolean; + display_name: string; + type: ModelType; + base_url: string; + use_full_url: boolean; + api_key: string; + tooltip_data: string; + model_id: string; reasoning_effort: string | null; - supports_image_generation: boolean; + openai_endpoint: string; + openai_extra_params_enabled: boolean; + openai_extra_params: Record; + custom_headers_enabled: boolean; + custom_headers: Record; + anthropic_extra_params_enabled: boolean; + anthropic_extra_params: Record; + context_window_tokens: number | null; + max_completion_tokens: number | null; + anthropic_max_tokens: number | null; + anthropic_thinking_effort: string | null; + thinking_budget_tokens: number | null; created_at_ms: number; updated_at_ms: number; } export interface ModelInput { - model_id: string; - display_name: string; - endpoint_type: ProviderType; - request_url: string; - enabled: boolean; sort_order: number; - context_window_tokens: number | null; - max_output_tokens: number | null; - reasoning_enabled: boolean; + display_name: string; + type: ModelType; + base_url: string; + use_full_url: boolean; + api_key: string; + tooltip_data: string; + model_id: string; reasoning_effort: string | null; - supports_image_generation: boolean; + openai_endpoint: string; + openai_extra_params_enabled: boolean; + openai_extra_params: Record; + custom_headers_enabled: boolean; + custom_headers: Record; + anthropic_extra_params_enabled: boolean; + anthropic_extra_params: Record; + context_window_tokens: number | null; + max_completion_tokens: number | null; + anthropic_max_tokens: number | null; + anthropic_thinking_effort: string | null; + thinking_budget_tokens: number | null; +} + +export interface ModelDiscoveryInput { + type: ModelType; + base_url: string; + api_key: string; + custom_headers_enabled: boolean; + custom_headers: Record; +} + +export interface LegacyModelImportPreviewItem { + model_hash: string; + display_name: string; + model_id: string; + type: ModelType; + existing: boolean; +} + +export interface LegacyModelImportPreview { + source: string; + total: number; + new_models: number; + existing_models: number; + models: LegacyModelImportPreviewItem[]; +} + +export interface LegacyModelImportResult { + imported: number; + skipped: number; + total: number; } export interface ModelConnectivityResult { @@ -146,10 +173,6 @@ export interface Overview { token_usage_series: OverviewTokenUsageBucket[]; } -export type ProviderSelection = - | { kind: "existing"; provider_id: number } - | { kind: "new"; input: ProviderInput }; - export interface LlmCall { call_kind: "provider_llm" | "cursor_official"; route: "local_byok" | "cursor_official"; @@ -260,29 +283,25 @@ export const api = { }); }, dismissAd: (id: string, reason: string) => request(`/ads/${encodeURIComponent(id)}/dismissals`, { method: "POST", body: JSON.stringify({ reason }) }), - providers: () => request("/providers"), - createProvider: (input: ProviderInput) => request("/providers", { method: "POST", body: JSON.stringify(input) }), - updateProvider: (id: number, input: ProviderInput) => request(`/providers/${id}`, { method: "PUT", body: JSON.stringify(input) }), - deleteProvider: (id: number) => request(`/providers/${id}`, { method: "DELETE" }), - discoverModels: (id: number) => request<{ models: string[] }>(`/providers/${id}/models/discover`, { method: "POST" }), - saveModels: (id: number, models: ModelInput[]) => request(`/providers/${id}/models`, { method: "POST", body: JSON.stringify({ models }) }), models: () => request("/models"), + createModels: (models: ModelInput[]) => request("/models", { method: "POST", body: JSON.stringify({ models }) }), + reorderModels: (modelHashes: string[]) => request("/models/order", { method: "PUT", body: JSON.stringify({ model_hashes: modelHashes }) }), + discoverModels: (input: ModelDiscoveryInput) => request<{ models: string[] }>("/models/discover", { method: "POST", body: JSON.stringify(input) }), + previewV0049Models: () => request("/models/import-v0049"), + importV0049Models: () => request("/models/import-v0049", { method: "POST" }), updateModel: (hash: string, model: ModelInput) => request(`/models/${hash}`, { method: "PUT", body: JSON.stringify(model) }), deleteModel: (hash: string) => request(`/models/${hash}`, { method: "DELETE" }), testModel: (hash: string) => request(`/models/${hash}/test`, { method: "POST" }), - overview: (filter?: { startMs: number; endMs: number; modelHashes?: string[]; providerIds?: number[] }) => { + overview: (filter?: { startMs: number; endMs: number; modelHashes?: string[] }) => { const params = new URLSearchParams(); if (filter) { params.set("start_ms", String(filter.startMs)); params.set("end_ms", String(filter.endMs)); if (filter.modelHashes?.length) params.set("model_hashes", JSON.stringify(filter.modelHashes)); - if (filter.providerIds?.length) params.set("provider_ids", JSON.stringify(filter.providerIds)); } const query = params.size ? `?${params}` : ""; return request(`/overview${query}`); }, - createCursorModels: (provider: ProviderSelection, models: ModelInput[]) => request<{ provider: Provider; models: Model[] }>("/harness/cursor/models", { method: "POST", body: JSON.stringify({ provider, models }) }), - discoverCursorModels: (provider: ProviderSelection) => request<{ models: string[] }>("/harness/cursor/models/discover", { method: "POST", body: JSON.stringify({ provider }) }), cursorHarness: () => request("/harness/cursor/status"), initializeCursorCa: () => request("/harness/cursor/ca/initialize", { method: "POST" }), openCursorCaInstallTerminal: async (command: string) => { diff --git a/apps/desktop/src/components/ProviderEditor.module.scss b/apps/desktop/src/components/ProviderEditor.module.scss deleted file mode 100644 index 6887402..0000000 --- a/apps/desktop/src/components/ProviderEditor.module.scss +++ /dev/null @@ -1,15 +0,0 @@ -.form { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - -.fullWidth { - width: 100%; - grid-column: 1 / -1; -} - -@media (max-width: 720px) { - .form { grid-template-columns: 1fr; } - .fullWidth { grid-column: auto; } -} diff --git a/apps/desktop/src/components/ProviderEditor.tsx b/apps/desktop/src/components/ProviderEditor.tsx deleted file mode 100644 index 9369f0e..0000000 --- a/apps/desktop/src/components/ProviderEditor.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { ProviderInput, ProviderType } from "../api"; -import { FormField, SecretTextInput, TextInput } from "./ui/FormControls"; -import { JsonEditor } from "./ui/JsonEditor"; -import { Select } from "./ui/Select"; -import { claudeIcon, openAiIcon } from "./ui/icons"; -import styles from "./ProviderEditor.module.scss"; - -export function ProviderEditor({ value, headersText, extraText, editing, onChange, onHeadersChange, onExtraChange }: { - value: ProviderInput; - headersText: string; - extraText: string; - editing: boolean; - onChange: (value: ProviderInput) => void; - onHeadersChange: (value: string) => void; - onExtraChange: (value: string) => void; -}) { - const patch = (next: Partial) => onChange({ ...value, ...next }); - return
- patch({ name: event.target.value })} /> - ({ value: String(provider.provider_id), label: provider.name })), - ]} onChange={selectProvider} />} + const numberValue = (value: string) => value === "" ? null : Math.trunc(Number(value)); + const canDiscover = Boolean(draft.model.base_url.trim() && draft.model.api_key.trim()); + const requestUrlPlaceholder = draft.model.use_full_url + ? draft.model.type === "anthropic" + ? "https://api.anthropic.com/v1/messages" + : draft.model.openai_endpoint === "/v1/chat/completions" + ? "https://api.openai.com/v1/chat/completions" + : "https://api.openai.com/v1/responses" + : draft.model.type === "anthropic" + ? "https://api.anthropic.com" + : "https://api.openai.com"; + return
- {!editing && draft.providerMode === "new" && <> - setProvider({ base_url: event.target.value })} /> - setProvider({ api_key: event.target.value })} /> - } - setType(value as ModelType)} /> + {draft.model.type === "openai" && setModel({ reasoning_effort: value || null })} /> + : <> + setModel({ anthropic_max_tokens: numberValue(event.target.value) })} /> + onChange({ ...settings, mode: mode as TabMode })} - /> -
-
- {settings.mode === "custom" &&
-
- {t("TAB 服务地址")} - {t("原接口路径会追加到此服务地址。")} -
- onChange({ ...settings, address: event.target.value })} - onKeyDown={(event) => { if (event.key === "Enter") onSave(); }} - /> -
} - ; -} diff --git a/apps/desktop/src/components/models/LegacyModelImport.module.scss b/apps/desktop/src/components/models/LegacyModelImport.module.scss new file mode 100644 index 0000000..c61a65b --- /dev/null +++ b/apps/desktop/src/components/models/LegacyModelImport.module.scss @@ -0,0 +1,92 @@ +@use "../../styles/typography" as type; + +.content { + display: grid; + gap: 14px; +} + +.source { + display: grid; + gap: 6px; + + code { + overflow-wrap: anywhere; + color: var(--vscode-descriptionForeground); + font-size: type.$font-size-xs; + } +} + +.counts { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + + > div { + display: grid; + gap: 3px; + padding: 10px; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + border-radius: 5px; + } + + small { + color: var(--vscode-descriptionForeground); + font-size: type.$font-size-xs; + } +} + +.models { + display: grid; + gap: 6px; +} + +.model { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + min-width: 0; + padding: 9px 10px; + background: var(--vscode-list-inactiveSelectionBackground); + border-radius: 5px; + + > div { + display: grid; + min-width: 0; + gap: 3px; + } + + small { + overflow: hidden; + color: var(--vscode-descriptionForeground); + font-size: type.$font-size-xs; + text-overflow: ellipsis; + white-space: nowrap; + } + + > span { + flex: 0 0 auto; + font-size: type.$font-size-xs; + } +} + +.new { + color: var(--vscode-testing-iconPassed, #73c991); +} + +.existing, +.hint { + color: var(--vscode-descriptionForeground); +} + +.hint { + font-size: type.$font-size-xs; +} + +@media (max-width: 680px) { + .model { + align-items: stretch; + flex-direction: column; + } +} diff --git a/apps/desktop/src/components/models/LegacyModelImport.tsx b/apps/desktop/src/components/models/LegacyModelImport.tsx new file mode 100644 index 0000000..4e5b3ef --- /dev/null +++ b/apps/desktop/src/components/models/LegacyModelImport.tsx @@ -0,0 +1,92 @@ +import { useState, type ReactNode } from "react"; +import { api, type LegacyModelImportPreview } from "../../api"; +import { appStore } from "../../store/appStore"; +import { ConfirmDialog } from "../ui/ConfirmDialog"; +import { useMessage } from "../ui/message"; +import styles from "./LegacyModelImport.module.scss"; + +type LegacyModelImportControl = { + busy: boolean; + previewing: boolean; + open: () => void; +}; + +export function LegacyModelImport({ children }: { children: (control: LegacyModelImportControl) => ReactNode }) { + const message = useMessage(); + const [preview, setPreview] = useState(null); + const [previewing, setPreviewing] = useState(false); + const [importing, setImporting] = useState(false); + + const open = async () => { + try { + setPreviewing(true); + setPreview(await api.previewV0049Models()); + } catch (cause) { + message(errorText(cause)); + } finally { + setPreviewing(false); + } + }; + + const confirm = async () => { + try { + setImporting(true); + const result = await appStore.importV0049Models(); + if (!result) { + const error = appStore.getSnapshot().error; + if (error) message(error); + return; + } + setPreview(null); + message(result.imported > 0 + ? t("导入完成:新增 {imported} 个模型,跳过 {skipped} 个已存在模型", { imported: result.imported, skipped: result.skipped }) + : t("配置中的 {count} 个模型均已存在,无需重复导入", { count: result.skipped })); + } catch (cause) { + message(errorText(cause)); + } finally { + setImporting(false); + } + }; + + return <> + {children({ busy: previewing || importing, previewing, open: () => void open() })} + setPreview(null)} + onConfirm={() => void confirm()} + > + {preview &&
+
+ {t("配置文件")} + {preview.source} +
+
+
{preview.total}{t("配置模型")}
+
{preview.new_models}{t("将新增")}
+
{preview.existing_models}{t("已存在")}
+
+
+ {preview.models.map((model) =>
+
+ {model.display_name} + {model.model_id} · {model.type === "openai" ? "OpenAI" : "Anthropic"} +
+ + {model.existing ? t("已存在,跳过") : t("新增")} + +
)} +
+ {t("重复导入相同配置不会创建重复模型;已经存在的模型会自动跳过。")} +
} +
+ ; +} + +function errorText(cause: unknown) { + return cause instanceof Error ? cause.message : String(cause); +} diff --git a/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx b/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx index 6cc03a4..919c72c 100644 --- a/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx +++ b/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx @@ -11,22 +11,19 @@ import styles from "./OverviewTimeRangeFilter.module.scss"; export type OverviewRangePreset = "ten-minutes" | "hour" | "today" | "week" | "month" | "custom"; -export function OverviewTimeRangeFilter({ value, customOpen, customStart, customEnd, modelOptions, providerOptions, selectedModels, selectedProviders, busy, onSelect, onCustomOpenChange, onCustomStartChange, onCustomEndChange, onSelectedModelsChange, onSelectedProvidersChange, onCustomApply, onRefresh }: { +export function OverviewTimeRangeFilter({ value, customOpen, customStart, customEnd, modelOptions, selectedModels, busy, onSelect, onCustomOpenChange, onCustomStartChange, onCustomEndChange, onSelectedModelsChange, onCustomApply, onRefresh }: { value: OverviewRangePreset; customOpen: boolean; customStart: string; customEnd: string; modelOptions: MultiSelectOption[]; - providerOptions: MultiSelectOption[]; selectedModels: string[]; - selectedProviders: string[]; busy: boolean; onSelect: (value: Exclude) => void; onCustomOpenChange: (open: boolean) => void; onCustomStartChange: (value: string) => void; onCustomEndChange: (value: string) => void; onSelectedModelsChange: (value: string[]) => void; - onSelectedProvidersChange: (value: string[]) => void; onCustomApply: () => void; onRefresh: () => void; }) { @@ -109,7 +106,6 @@ export function OverviewTimeRangeFilter({ value, customOpen, customStart, custom
-
diff --git a/apps/desktop/src/components/settings/TabSettingsCard.module.scss b/apps/desktop/src/components/settings/TabSettingsCard.module.scss new file mode 100644 index 0000000..da9f8ba --- /dev/null +++ b/apps/desktop/src/components/settings/TabSettingsCard.module.scss @@ -0,0 +1,78 @@ +@use "../../styles/typography" as type; + +.title { + min-width: 0; + display: flex; + align-items: center; + gap: 5px; +} + +.content { + display: grid; + gap: 14px; + padding: 16px; +} + +.row { + min-width: 0; + min-height: 34px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; +} + +.description { + min-width: 0; + display: grid; + gap: 5px; + + small { + color: var(--vscode-descriptionForeground); + font-size: type.$font-size-xs; + } +} + +.control { + width: min(420px, 62%); +} + +.value { + min-width: 0; + max-width: 70%; + color: var(--vscode-descriptionForeground); + overflow-wrap: anywhere; + text-align: right; +} + +.headerAction { + padding: 4px; + color: var(--vscode-textLink-foreground); + background: transparent; + border: 0; + font-size: type.$font-size-xs; + + &:hover { color: var(--vscode-textLink-activeForeground); } + &:disabled { opacity: .5; } +} + +.actionGroup { + display: flex; + align-items: center; + gap: 6px; +} + +@media (max-width: 680px) { + .row { + align-items: stretch; + flex-direction: column; + gap: 10px; + } + + .control, + .value { + width: 100%; + max-width: none; + text-align: left; + } +} diff --git a/apps/desktop/src/components/settings/TabSettingsCard.tsx b/apps/desktop/src/components/settings/TabSettingsCard.tsx new file mode 100644 index 0000000..f380a78 --- /dev/null +++ b/apps/desktop/src/components/settings/TabSettingsCard.tsx @@ -0,0 +1,90 @@ +import cursorIconUrl from "../../assets/icons/cursor.svg"; +import type { TabMode, TabSettings } from "../../api"; +import { Button } from "../ui/Button"; +import { TextInput } from "../ui/FormControls"; +import { Icon } from "../ui/Icon"; +import { Select } from "../ui/Select"; +import { TitledCard } from "../ui/TitledCard"; +import styles from "./TabSettingsCard.module.scss"; + +export function TabSettingsCard({ + settings, + draft, + editing, + saving, + onDraftChange, + onEdit, + onCancel, + onSave, +}: { + settings: TabSettings | null; + draft: TabSettings; + editing: boolean; + saving: boolean; + onDraftChange: (settings: TabSettings) => void; + onEdit: () => void; + onCancel: () => void; + onSave: () => void; +}) { + const modeLabel = (mode: TabMode) => { + if (mode === "public") return t("使用公益服务"); + if (mode === "direct") return t("直连"); + return t("自定义"); + }; + const action = editing ? ( +
+ + +
+ ) : ( + + ); + + return {t("TAB 设置")}
} + action={action} + > +
+ {editing ? <> +
+
+ {t("TAB 选择")} + {t("控制 Cursor TAB 相关接口的连接方式。")} +
+