diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index a6cd22d..d92b9f6 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef } from "react"; import { HashRouter, Navigate, Route, Routes } from "react-router-dom"; +import { TooltipProvider } from "./shared/ui/Tooltip"; import { MessageProvider } from "./shared/ui/MessageProvider"; import { useMessage } from "./shared/ui/message"; import { AppFrame } from "./shell/AppFrame"; @@ -17,7 +18,7 @@ const AUTO_UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1_000; export function App() { return ( - <> + } /> @@ -34,7 +35,7 @@ export function App() { - + ); } diff --git a/apps/desktop/src/features/home/HomePage.tsx b/apps/desktop/src/features/home/HomePage.tsx index ae6973f..de81482 100644 --- a/apps/desktop/src/features/home/HomePage.tsx +++ b/apps/desktop/src/features/home/HomePage.tsx @@ -15,6 +15,25 @@ import { useI18n } from "../../i18n/store"; type TimeRange = { startMs: number; endMs: number }; +const CALENDAR_DAYS = 365; +const DAY_MS = 24 * 60 * 60_000; + +function contributionCalendarData(overview: Overview, endMs: number) { + const tokensByDate = new Map(); + for (const bucket of overview.token_usage_series) { + const date = new Date(bucket.bucket_start_ms).toISOString().slice(0, 10); + const tokens = bucket.input_tokens + bucket.cache_read_tokens + bucket.cache_write_tokens + bucket.output_tokens; + tokensByDate.set(date, (tokensByDate.get(date) ?? 0) + tokens); + } + const lastDay = new Date(Math.max(0, endMs - 1)); + lastDay.setUTCHours(0, 0, 0, 0); + const firstDayMs = lastDay.getTime() - (CALENDAR_DAYS - 1) * DAY_MS; + return Array.from({ length: CALENDAR_DAYS }, (_, offset) => { + const date = new Date(firstDayMs + offset * DAY_MS).toISOString().slice(0, 10); + return { date, tokens: tokensByDate.get(date) ?? 0 }; + }); +} + function presetRange(preset: Exclude, now = new Date()): TimeRange { const endMs = now.getTime(); if (preset === "today") { @@ -74,10 +93,7 @@ export function HomePage() { cacheWriteTokens: bucket.cache_write_tokens, outputTokens: bucket.output_tokens, })); - const contribution = overview.token_usage_series.map((bucket) => ({ - date: new Date(bucket.bucket_start_ms).toISOString().slice(0, 10), - tokens: bucket.input_tokens + bucket.cache_read_tokens + bucket.cache_write_tokens + bucket.output_tokens, - })); + const contribution = contributionCalendarData(filteredOverview, selectedRange?.endMs ?? Date.now()); const metrics = { llmCalls: filteredOverview.metrics.llm_calls, successfulCalls: filteredOverview.metrics.successful_calls, @@ -107,13 +123,15 @@ export function HomePage() { setCustomOpen(false); }; const selectQuick = (durationMs: number, bucketMs?: number) => { - const endMs = Date.now(); - setCustomRange({ startMs: endMs - durationMs, endMs }); + const end = new Date(); + const start = new Date(end.getTime() - durationMs); + setCustomStart(formatTimeInput(start)); + setCustomEnd(formatTimeInput(end)); + setCustomRange({ startMs: start.getTime(), endMs: end.getTime() }); setAppliedModels(selectedModels); setQuick(durationMs === 4 * 60 * 60_000 ? "four-hours" : "twenty-four-hours"); setFourHourBucket(durationMs === 4 * 60 * 60_000 ? bucketMs : undefined); setPreset("custom"); - setCustomOpen(false); }; const selectPreset = (value: Exclude) => { setPreset(value); diff --git a/apps/desktop/src/features/home/charts/ContributionCalendarChart.tsx b/apps/desktop/src/features/home/charts/ContributionCalendarChart.tsx index 5f37561..88b98b2 100644 --- a/apps/desktop/src/features/home/charts/ContributionCalendarChart.tsx +++ b/apps/desktop/src/features/home/charts/ContributionCalendarChart.tsx @@ -2,7 +2,7 @@ import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { init, Rect, type ElementEvent } from "zrender"; import type { Locale } from "../../../i18n/runtime"; import { useI18n } from "../../../i18n/store"; -import { Tooltip, type TooltipAnchor } from "../../../shared/ui/Tooltip"; +import { useTooltip, type TooltipAnchor } from "../../../shared/ui/Tooltip"; import styles from "./ContributionCalendarChart.module.scss"; export type ContributionDay = { @@ -28,12 +28,6 @@ type CellExtra = CalendarCell & { height: number; }; -type TooltipState = { - date: string; - tokens: number; - anchor: TooltipAnchor; -}; - type AxisLabel = { key: string; text: string; @@ -101,7 +95,7 @@ export function ContributionCalendarChart({ data }: ContributionCalendarChartPro const canvasRef = useRef(null); const layoutRef = useRef>(null); const scheduleDrawRef = useRef<() => void>(() => undefined); - const [tooltip, setTooltip] = useState(null); + const { show: showTooltip, hide: hideTooltip } = useTooltip(); const [axisLabels, setAxisLabels] = useState([]); const layout = useMemo(() => buildCalendarLayout(data, locale), [data, locale]); const tokenFormatter = useMemo(() => new Intl.NumberFormat(locale), [locale]); @@ -129,10 +123,13 @@ export function ContributionCalendarChart({ data }: ContributionCalendarChartPro return new DOMRect(bounds.left + extra.x, bounds.top + extra.y, extra.width, extra.height); }, }; - setTooltip({ date: extra.date, tokens: extra.tokens, anchor }); + showTooltip(anchor, undefined,
+ {extra.date} + {t("Token 用量:{tokens}", { tokens: tokenFormatter.format(extra.tokens) })} +
); }; const handleMouseOut = (event: ElementEvent) => { - if (isCellExtra(event.target?.extra)) setTooltip(null); + if (isCellExtra(event.target?.extra)) hideTooltip(); }; chart.on("mouseover", handleMouseOver); @@ -226,7 +223,7 @@ export function ContributionCalendarChart({ data }: ContributionCalendarChartPro cellRects.set(cell.date, rect); chart.add(rect); } - setTooltip(null); + hideTooltip(); }; const scheduleDraw = () => { window.cancelAnimationFrame(drawFrame); @@ -264,12 +261,6 @@ export function ContributionCalendarChart({ data }: ContributionCalendarChartPro {axisLabels.map((label) => {label.text})} - -
- {tooltip?.date} - {t("Token 用量:{tokens}", { tokens: tokenFormatter.format(tooltip?.tokens ?? 0) })} -
-
); } diff --git a/apps/desktop/src/features/home/metrics/HomeMetrics.tsx b/apps/desktop/src/features/home/metrics/HomeMetrics.tsx index 19607b4..995c405 100644 --- a/apps/desktop/src/features/home/metrics/HomeMetrics.tsx +++ b/apps/desktop/src/features/home/metrics/HomeMetrics.tsx @@ -1,7 +1,6 @@ -import { useState } from "react"; import { formatCompactInteger, formatInteger } from "../../../shared/utils/numberFormat"; import { Icon } from "../../../shared/ui/Icon"; -import { Tooltip, type TooltipAnchor } from "../../../shared/ui/Tooltip"; +import { useTooltip, type TooltipAnchor } from "../../../shared/ui/Tooltip"; import { informationOutlineIcon } from "../../../shared/ui/icons"; import { CacheHitRateChart } from "./CacheHitRateChart"; import styles from "./HomeMetrics.module.scss"; @@ -53,22 +52,17 @@ function elementAnchor(element: HTMLElement): TooltipAnchor { } function InfoTooltip({ content }: { content: string }) { - const [anchor, setAnchor] = useState(null); + const { show, hide } = useTooltip(); - return <> - - -
{content}
-
- ; + return ; } export function HomeMetrics({ data, refreshVersion = 0 }: { data: HomeMetricsData; refreshVersion?: number }) { diff --git a/apps/desktop/src/features/home/overview/OverviewTimeRangeFilter.tsx b/apps/desktop/src/features/home/overview/OverviewTimeRangeFilter.tsx index f76c761..bac533c 100644 --- a/apps/desktop/src/features/home/overview/OverviewTimeRangeFilter.tsx +++ b/apps/desktop/src/features/home/overview/OverviewTimeRangeFilter.tsx @@ -157,7 +157,10 @@ export function OverviewTimeRangeFilter({ value, quick, fourHourBucket, customOp aria-controls={granularityOpen ? granularityMenuId : undefined} aria-expanded={granularityOpen} aria-pressed={quick === "four-hours"} - onClick={() => setGranularityOpen((current) => !current)} + onClick={() => { + onQuickSelect(4 * 60 * 60_000, fourHourBucket); + setGranularityOpen((current) => !current); + }} >{preset.label} : } + {provider.hasModels &&
+ + +
} ; } +function ModelManagementModal({ provider, busy, onClose, onSubmit }: { + provider: PluginProviderDescriptor; + busy: boolean; + onClose: () => void; + onSubmit: (enabledByModel: Record) => void; +}) { + const { locale } = useI18n(); + const [enabledByModel, setEnabledByModel] = useState>({}); + + useEffect(() => { + setEnabledByModel(Object.fromEntries(provider.models.map((model) => [model.id, model.enabled]))); + }, [provider.models]); + + const setAll = (enabled: boolean) => { + setEnabledByModel(Object.fromEntries(provider.models.map((model) => [model.id, enabled]))); + }; + + return onSubmit(enabledByModel)} + submitLabel={t("确定")} + > +
+ + +
+
+ + + + {provider.models.map((model) => + + + )} + +
{t("模型名称")}{t("启用")}
+ {model.displayName} + {model.description && {model.description}} +
setEnabledByModel((current) => ({ ...current, [model.id]: enabled }))} + />
+ {provider.models.length === 0 && {t("尚未同步模型")}} +
+
; +} + function ResourceList({ resource, busy, onRefresh, onDelete }: { resource: PluginResourceDescriptor; busy: boolean; diff --git a/apps/desktop/src/i18n/generated/catalog.json b/apps/desktop/src/i18n/generated/catalog.json index 5aaa7dd..902327b 100644 --- a/apps/desktop/src/i18n/generated/catalog.json +++ b/apps/desktop/src/i18n/generated/catalog.json @@ -21,7 +21,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 103, + "line": 97, "column": 5 } ] @@ -33,7 +33,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 152, + "line": 157, "column": 40 } ] @@ -47,7 +47,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 196, + "line": 214, "column": 39 } ] @@ -63,7 +63,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 123, + "line": 117, "column": 5 } ] @@ -78,7 +78,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 243, + "line": 317, "column": 16 } ] @@ -94,7 +94,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 138, + "line": 132, "column": 5 } ] @@ -158,7 +158,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 151, + "line": 153, "column": 27 } ] @@ -196,7 +196,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 115, + "line": 109, "column": 5 } ] @@ -210,7 +210,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 112, + "line": 106, "column": 5 } ] @@ -222,12 +222,12 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 182, + "line": 187, "column": 16 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 386, + "line": 388, "column": 73 } ] @@ -266,7 +266,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 397, + "line": 399, "column": 89 } ] @@ -278,7 +278,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 171, + "line": 165, "column": 40 } ] @@ -294,7 +294,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 133, + "line": 127, "column": 5 } ] @@ -306,8 +306,13 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 196, + "line": 214, "column": 93 + }, + { + "file": "features/plugins/PluginResourcePanels.tsx", + "line": 276, + "column": 72 } ] }, @@ -464,7 +469,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 164, + "line": 158, "column": 40 } ] @@ -476,12 +481,12 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 169, + "line": 174, "column": 16 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 396, + "line": 398, "column": 36 } ] @@ -493,7 +498,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 320, + "line": 322, "column": 213 } ] @@ -517,7 +522,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 150, + "line": 144, "column": 40 } ] @@ -529,8 +534,8 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 154, - "column": 286 + "line": 159, + "column": 322 } ] }, @@ -541,12 +546,12 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 143, + "line": 148, "column": 59 }, { "file": "features/models/CursorModelEditor.tsx", - "line": 143, + "line": 148, "column": 124 } ] @@ -563,17 +568,17 @@ }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 324, + "line": 326, "column": 51 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 324, + "line": 326, "column": 130 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 339, + "line": 341, "column": 74 } ] @@ -587,7 +592,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 102, + "line": 96, "column": 5 } ] @@ -684,7 +689,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 124, + "line": 127, "column": 15 } ] @@ -713,7 +718,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 360, + "line": 362, "column": 52 } ] @@ -739,7 +744,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 110, + "line": 104, "column": 5 } ] @@ -773,7 +778,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 93, + "line": 87, "column": 5 } ] @@ -812,17 +817,17 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 158, + "line": 163, "column": 49 }, { "file": "features/models/CursorModelEditor.tsx", - "line": 160, + "line": 165, "column": 50 }, { "file": "features/models/CursorModelEditor.tsx", - "line": 163, + "line": 168, "column": 50 } ] @@ -836,7 +841,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 390, + "line": 392, "column": 89 } ] @@ -872,7 +877,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 318, + "line": 320, "column": 136 } ] @@ -936,7 +941,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 154, + "line": 159, "column": 42 } ] @@ -986,7 +991,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 100, + "line": 94, "column": 5 } ] @@ -998,12 +1003,12 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 165, + "line": 170, "column": 27 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 389, + "line": 391, "column": 186 } ] @@ -1015,7 +1020,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 132, + "line": 135, "column": 73 } ] @@ -1027,17 +1032,17 @@ "refs": [ { "file": "features/home/overview/OverviewTimeRangeFilter.tsx", - "line": 176, + "line": 179, "column": 105 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 329, + "line": 331, "column": 20 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 360, + "line": 362, "column": 76 }, { @@ -1109,7 +1114,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 328, + "line": 330, "column": 14 } ] @@ -1123,7 +1128,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 92, + "line": 86, "column": 5 } ] @@ -1152,7 +1157,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 155, + "line": 160, "column": 42 } ] @@ -1166,7 +1171,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 403, + "line": 405, "column": 70 } ] @@ -1236,12 +1241,12 @@ }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 360, + "line": 362, "column": 99 }, { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 271, + "line": 345, "column": 68 } ] @@ -1253,7 +1258,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 281, + "line": 355, "column": 81 } ] @@ -1265,12 +1270,12 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 228, + "line": 302, "column": 32 }, { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 228, + "line": 302, "column": 56 } ] @@ -1282,12 +1287,12 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 353, + "line": 355, "column": 35 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 356, + "line": 358, "column": 41 } ] @@ -1299,7 +1304,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 136, + "line": 139, "column": 64 } ] @@ -1345,7 +1350,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 388, + "line": 390, "column": 123 } ] @@ -1377,7 +1382,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 157, + "line": 151, "column": 46 } ] @@ -1464,6 +1469,11 @@ "kind": "text", "placeholders": [], "refs": [ + { + "file": "features/plugins/PluginResourcePanels.tsx", + "line": 255, + "column": 106 + }, { "file": "shared/ui/ModelSelect.tsx", "line": 232, @@ -1516,8 +1526,8 @@ "refs": [ { "file": "features/home/charts/ContributionCalendarChart.tsx", - "line": 270, - "column": 18 + "line": 128, + "column": 16 } ] }, @@ -1569,7 +1579,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 320, + "line": 322, "column": 199 } ] @@ -1581,7 +1591,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 344, + "line": 346, "column": 80 }, { @@ -1634,7 +1644,7 @@ "refs": [ { "file": "features/home/overview/OverviewTimeRangeFilter.tsx", - "line": 185, + "line": 188, "column": 19 } ] @@ -1706,7 +1716,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 160, + "line": 162, "column": 27 } ] @@ -1720,7 +1730,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 105, + "line": 99, "column": 5 } ] @@ -1744,7 +1754,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 132, + "line": 135, "column": 92 } ] @@ -1818,7 +1828,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 33, + "line": 32, "column": 27 }, { @@ -1835,12 +1845,12 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 158, + "line": 163, "column": 25 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 389, + "line": 391, "column": 34 } ] @@ -1867,7 +1877,7 @@ }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 339, + "line": 341, "column": 703 } ] @@ -1915,7 +1925,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 149, + "line": 154, "column": 145 } ] @@ -1944,7 +1954,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 283, + "line": 357, "column": 42 } ] @@ -1956,7 +1966,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 207, + "line": 212, "column": 41 } ] @@ -2026,7 +2036,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 197, + "line": 199, "column": 13 } ] @@ -2038,7 +2048,7 @@ "refs": [ { "file": "features/home/overview/OverviewTimeRangeFilter.tsx", - "line": 173, + "line": 176, "column": 70 } ] @@ -2050,7 +2060,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 304, + "line": 306, "column": 68 }, { @@ -2103,7 +2113,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 255, + "line": 257, "column": 47 } ] @@ -2139,7 +2149,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 324, + "line": 326, "column": 63 } ] @@ -2151,17 +2161,17 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 160, + "line": 165, "column": 27 }, { "file": "features/models/CursorModelEditor.tsx", - "line": 163, + "line": 168, "column": 27 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 389, + "line": 391, "column": 83 } ] @@ -2185,7 +2195,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 156, + "line": 161, "column": 69 } ] @@ -2262,7 +2272,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 319, + "line": 321, "column": 124 } ] @@ -2338,11 +2348,23 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 144, + "line": 138, "column": 5 } ] }, + "6003d3246f0fca2b": { + "source": "模型管理", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "features/plugins/PluginResourcePanels.tsx", + "line": 220, + "column": 102 + } + ] + }, "6078a681a306930d": { "source": "缓存写入", "kind": "text", @@ -2381,7 +2403,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 104, + "line": 98, "column": 5 } ] @@ -2407,7 +2429,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 114, + "line": 108, "column": 5 } ] @@ -2436,7 +2458,7 @@ "refs": [ { "file": "features/home/overview/OverviewTimeRangeFilter.tsx", - "line": 177, + "line": 180, "column": 109 } ] @@ -2458,11 +2480,25 @@ }, { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 127, + "line": 130, "column": 30 } ] }, + "6449a43900b609a4": { + "source": "{name} 模型管理", + "kind": "template", + "placeholders": [ + "name" + ], + "refs": [ + { + "file": "features/plugins/PluginResourcePanels.tsx", + "line": 248, + "column": 12 + } + ] + }, "6478a5f1218c484e": { "source": "请在桌面应用中复制到系统剪贴板", "kind": "text", @@ -2470,7 +2506,7 @@ "refs": [ { "file": "shared/api.ts", - "line": 487, + "line": 490, "column": 43 } ] @@ -2556,7 +2592,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 167, + "line": 161, "column": 46 } ] @@ -2621,7 +2657,7 @@ "refs": [ { "file": "features/home/overview/OverviewTimeRangeFilter.tsx", - "line": 172, + "line": 175, "column": 21 } ] @@ -2708,7 +2744,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 174, + "line": 168, "column": 46 } ] @@ -2720,7 +2756,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 317, + "line": 319, "column": 105 } ] @@ -2797,7 +2833,7 @@ "refs": [ { "file": "shared/api.ts", - "line": 482, + "line": 485, "column": 43 } ] @@ -2809,7 +2845,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 148, + "line": 142, "column": 50 } ] @@ -2845,12 +2881,12 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 176, + "line": 181, "column": 16 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 384, + "line": 386, "column": 67 } ] @@ -2864,7 +2900,7 @@ "refs": [ { "file": "App.tsx", - "line": 61, + "line": 62, "column": 21 } ] @@ -2927,7 +2963,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 264, + "line": 338, "column": 13 } ] @@ -2951,7 +2987,7 @@ "refs": [ { "file": "shared/api.ts", - "line": 418, + "line": 420, "column": 21 } ] @@ -2978,7 +3014,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 195, + "line": 197, "column": 11 } ] @@ -3005,8 +3041,8 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 154, - "column": 298 + "line": 159, + "column": 334 } ] }, @@ -3087,8 +3123,8 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 202, - "column": 31 + "line": 222, + "column": 33 } ] }, @@ -3126,8 +3162,8 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 62, - "column": 19 + "line": 60, + "column": 17 } ] }, @@ -3150,7 +3186,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 149, + "line": 154, "column": 115 } ] @@ -3162,7 +3198,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 244, + "line": 318, "column": 110 }, { @@ -3249,7 +3285,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 143, + "line": 148, "column": 76 } ] @@ -3261,7 +3297,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 127, + "line": 130, "column": 19 } ] @@ -3287,7 +3323,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 404, + "line": 406, "column": 87 } ] @@ -3361,7 +3397,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 304, + "line": 306, "column": 79 }, { @@ -3402,7 +3438,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 360, + "line": 362, "column": 250 } ] @@ -3414,8 +3450,8 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 202, - "column": 18 + "line": 222, + "column": 20 } ] }, @@ -3498,7 +3534,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 134, + "line": 137, "column": 147 } ] @@ -3520,12 +3556,12 @@ }, { "file": "features/models/CursorModelEditor.tsx", - "line": 164, + "line": 169, "column": 27 }, { "file": "features/models/CursorModelEditor.tsx", - "line": 164, + "line": 169, "column": 58 } ] @@ -3566,7 +3602,7 @@ "refs": [ { "file": "features/home/overview/OverviewTimeRangeFilter.tsx", - "line": 172, + "line": 175, "column": 70 } ] @@ -3623,7 +3659,7 @@ }, { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 86, + "line": 88, "column": 22 } ] @@ -3685,7 +3721,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 206, + "line": 208, "column": 22 } ] @@ -3738,12 +3774,12 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 339, + "line": 341, "column": 432 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 347, + "line": 349, "column": 193 }, { @@ -3837,7 +3873,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 134, + "line": 136, "column": 15 } ] @@ -3892,7 +3928,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 339, + "line": 341, "column": 715 } ] @@ -3926,7 +3962,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 150, + "line": 155, "column": 61 } ] @@ -3979,7 +4015,7 @@ }, { "file": "features/models/CursorModelEditor.tsx", - "line": 155, + "line": 160, "column": 25 } ] @@ -3989,6 +4025,11 @@ "kind": "text", "placeholders": [], "refs": [ + { + "file": "features/plugins/PluginResourcePanels.tsx", + "line": 256, + "column": 107 + }, { "file": "shared/ui/ModelSelect.tsx", "line": 233, @@ -4015,7 +4056,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 156, + "line": 161, "column": 118 } ] @@ -4055,7 +4096,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 128, + "line": 122, "column": 5 } ] @@ -4089,7 +4130,7 @@ }, { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 270, + "line": 344, "column": 84 }, { @@ -4167,7 +4208,7 @@ }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 347, + "line": 349, "column": 49 } ] @@ -4205,7 +4246,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 111, + "line": 105, "column": 5 } ] @@ -4270,7 +4311,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 113, + "line": 107, "column": 5 } ] @@ -4306,7 +4347,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 344, + "line": 346, "column": 103 } ] @@ -4376,7 +4417,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 155, + "line": 160, "column": 99 } ] @@ -4388,7 +4429,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 278, + "line": 352, "column": 81 } ] @@ -4403,7 +4444,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 198, + "line": 200, "column": 13 } ] @@ -4468,12 +4509,12 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 352, + "line": 354, "column": 45 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 355, + "line": 357, "column": 42 } ] @@ -4566,7 +4607,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 239, + "line": 313, "column": 66 } ] @@ -4578,7 +4619,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 339, + "line": 341, "column": 62 } ] @@ -4590,12 +4631,12 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 161, + "line": 166, "column": 27 }, { "file": "features/models/CursorModelEditor.tsx", - "line": 161, + "line": 166, "column": 58 } ] @@ -4619,7 +4660,7 @@ "refs": [ { "file": "features/home/overview/OverviewTimeRangeFilter.tsx", - "line": 174, + "line": 177, "column": 77 } ] @@ -4631,7 +4672,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 242, + "line": 316, "column": 102 }, { @@ -4655,7 +4696,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 121, + "line": 115, "column": 5 } ] @@ -4672,8 +4713,13 @@ }, { "file": "features/models/CursorModelEditor.tsx", - "line": 154, + "line": 159, "column": 25 + }, + { + "file": "features/plugins/PluginResourcePanels.tsx", + "line": 260, + "column": 37 } ] }, @@ -4754,7 +4800,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 165, + "line": 170, "column": 50 } ] @@ -4819,12 +4865,12 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 149, + "line": 154, "column": 70 }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 352, + "line": 354, "column": 27 } ] @@ -4836,7 +4882,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 339, + "line": 341, "column": 675 }, { @@ -4877,7 +4923,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 198, + "line": 216, "column": 32 } ] @@ -4889,17 +4935,17 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 158, + "line": 163, "column": 121 }, { "file": "features/models/CursorModelEditor.tsx", - "line": 160, + "line": 165, "column": 122 }, { "file": "features/models/CursorModelEditor.tsx", - "line": 163, + "line": 168, "column": 122 } ] @@ -4911,7 +4957,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 165, + "line": 170, "column": 137 } ] @@ -4923,7 +4969,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 337, + "line": 339, "column": 11 } ] @@ -4949,11 +4995,25 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 349, + "line": 351, "column": 27 } ] }, + "d896c62fb6712bda": { + "source": "启用 {model}", + "kind": "template", + "placeholders": [ + "model" + ], + "refs": [ + { + "file": "features/plugins/PluginResourcePanels.tsx", + "line": 270, + "column": 22 + } + ] + }, "d8c47e9776cf1082": { "source": "主菜单", "kind": "text", @@ -4985,7 +5045,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 345, + "line": 347, "column": 47 } ] @@ -5009,12 +5069,12 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 111, + "line": 116, "column": 88 }, { "file": "features/models/CursorModelEditor.tsx", - "line": 156, + "line": 161, "column": 54 } ] @@ -5091,7 +5151,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 108, + "line": 102, "column": 5 } ] @@ -5178,7 +5238,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 29, + "line": 31, "column": 71 } ] @@ -5216,7 +5276,7 @@ "refs": [ { "file": "features/home/charts/ContributionCalendarChart.tsx", - "line": 255, + "line": 252, "column": 50 } ] @@ -5240,7 +5300,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 218, + "line": 220, "column": 26 } ] @@ -5264,7 +5324,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 132, + "line": 135, "column": 34 } ] @@ -5276,7 +5336,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 94, + "line": 96, "column": 36 } ] @@ -5331,7 +5391,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 154, + "line": 148, "column": 40 } ] @@ -5343,7 +5403,7 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 149, + "line": 154, "column": 54 } ] @@ -5396,7 +5456,7 @@ "refs": [ { "file": "features/plugins/PluginResourcePanels.tsx", - "line": 198, + "line": 216, "column": 43 } ] @@ -5425,12 +5485,12 @@ "refs": [ { "file": "features/models/CursorModelEditor.tsx", - "line": 142, + "line": 147, "column": 25 }, { "file": "features/models/CursorModelEditor.tsx", - "line": 142, + "line": 147, "column": 55 } ] @@ -5502,7 +5562,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 120, + "line": 114, "column": 5 } ] @@ -5514,7 +5574,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 349, + "line": 351, "column": 44 } ] @@ -5526,7 +5586,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 345, + "line": 347, "column": 77 } ] @@ -5555,7 +5615,7 @@ }, { "file": "features/models/CursorSettingsPage.tsx", - "line": 316, + "line": 318, "column": 98 } ] @@ -5567,7 +5627,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 344, + "line": 346, "column": 53 } ] @@ -5623,6 +5683,18 @@ } ] }, + "f4f0ead1116b5b62": { + "source": "启用", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "features/plugins/PluginResourcePanels.tsx", + "line": 260, + "column": 69 + } + ] + }, "f4fa9f31ea2ae58d": { "source": "过去一年的 Token 用量日历", "kind": "text", @@ -5630,7 +5702,7 @@ "refs": [ { "file": "features/home/charts/ContributionCalendarChart.tsx", - "line": 261, + "line": 258, "column": 23 } ] @@ -5642,7 +5714,7 @@ "refs": [ { "file": "features/home/overview/OverviewTimeRangeFilter.tsx", - "line": 173, + "line": 176, "column": 21 } ] @@ -5788,6 +5860,18 @@ } ] }, + "fac2a67ad87807c4": { + "source": "确定", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "features/plugins/PluginResourcePanels.tsx", + "line": 252, + "column": 18 + } + ] + }, "fad86bf65f72c747": { "source": "下载进度", "kind": "text", @@ -5856,7 +5940,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 94, + "line": 88, "column": 5 } ] @@ -5868,7 +5952,7 @@ "refs": [ { "file": "features/models/CursorSettingsPage.tsx", - "line": 330, + "line": 332, "column": 21 } ] @@ -5880,7 +5964,7 @@ "refs": [ { "file": "shared/ui/Select.tsx", - "line": 111, + "line": 142, "column": 90 } ] @@ -5892,7 +5976,7 @@ "refs": [ { "file": "features/home/metrics/HomeMetrics.tsx", - "line": 117, + "line": 111, "column": 5 } ] @@ -5928,7 +6012,7 @@ "refs": [ { "file": "features/home/HomePage.tsx", - "line": 188, + "line": 206, "column": 25 } ] diff --git a/apps/desktop/src/i18n/locales/en-US.json b/apps/desktop/src/i18n/locales/en-US.json index 28d11b2..2b8c65b 100644 --- a/apps/desktop/src/i18n/locales/en-US.json +++ b/apps/desktop/src/i18n/locales/en-US.json @@ -158,6 +158,7 @@ "5f8d556a9c47da3c": "Launch at login disabled", "5f9acfb945229062": "Are you sure you no longer want to see this ad?", "5fd2ec5a6e9b654c": "Total: {cost}", + "6003d3246f0fca2b": "Model management", "6078a681a306930d": "Cache write", "609640f72d422b57": "Quick time ranges", "61a4c7bac12dc125": "Failed calls: {count}", @@ -166,6 +167,7 @@ "6320b4a8722a851f": "Status", "63c73c4730f4473e": "Apply", "63d90d977348ab1f": "Duplicate", + "6449a43900b609a4": "{name} model management", "6478a5f1218c484e": "Use the desktop app to copy to the system clipboard", "651f274470153a05": "Software updates", "652ec5d40c29fd6a": "Speed {speed} tokens/s · first token {firstText} ms · total {duration} ms · output {tokens} tokens{estimated} · response: {output}", @@ -341,6 +343,7 @@ "d71b0171c44b668a": "Disabling this will remove Cursor's local proxy configuration. If you need to sign in to an official account, you usually do not need to disable it. You can sign in directly because BYOK models and official-account models now work together seamlessly. Do you want to continue disabling and clearing the proxy configuration?", "d766536c18e8e990": "Plugin runtime {version} is installed and ready to use.", "d7e266bdc8064193": "Group name", + "d896c62fb6712bda": "Enable {model}", "d8c47e9776cf1082": "Main menu", "d8c589c455675b46": "Prompt settings saved", "da521d1c1cbd36af": "Authorization is required to install the certificate", @@ -392,6 +395,7 @@ "ee6b89a6a740a4c4": "If a port is occupied, a new random port is selected and saved automatically. Restart the app after changing these settings.", "f4694c46b1e19602": "Final request type", "f4dcb6a3ceb32247": "Page {page} of {count}", + "f4f0ead1116b5b62": "Enabled", "f4fa9f31ea2ae58d": "Token usage calendar for the past year", "f50276449943286c": "End time", "f69273dbbebfb3a1": "Format", @@ -405,6 +409,7 @@ "f9aa11dbb15ce647": "Saturday", "f9b55ca75425161b": "Response content was not recorded. Enable detailed records and try again.", "fa5b4b8a751c7d1b": "The local proxy port used by Cursor. Enter 0 to select a random port at startup.", + "fac2a67ad87807c4": "", "fad86bf65f72c747": "Download progress", "fb11aa6f29827095": "Checking…", "fbe8778fa8b9bab5": "Initialize the local CA first", diff --git a/apps/desktop/src/i18n/locales/zh-CN.json b/apps/desktop/src/i18n/locales/zh-CN.json index f866b2f..0cac350 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.json +++ b/apps/desktop/src/i18n/locales/zh-CN.json @@ -158,6 +158,7 @@ "5f8d556a9c47da3c": "已关闭开机启动", "5f9acfb945229062": "你确认不想再看到此广告吗?", "5fd2ec5a6e9b654c": "合计:{cost}", + "6003d3246f0fca2b": "模型管理", "6078a681a306930d": "缓存写入", "609640f72d422b57": "快捷时间范围", "61a4c7bac12dc125": "异常调用:{count}", @@ -166,6 +167,7 @@ "6320b4a8722a851f": "状态", "63c73c4730f4473e": "应用", "63d90d977348ab1f": "复制", + "6449a43900b609a4": "{name} 模型管理", "6478a5f1218c484e": "请在桌面应用中复制到系统剪贴板", "651f274470153a05": "软件更新", "652ec5d40c29fd6a": "速度 {speed} tokens/s · 首字 {firstText} ms · 总耗时 {duration} ms · 输出 {tokens} tokens{estimated} · 返回:{output}", @@ -341,6 +343,7 @@ "d71b0171c44b668a": "关闭后将移除 Cursor 本地代理配置。如果你需要登陆官方账号,通常不需要关闭操作,推荐直接登陆你的账号即可(byok模型与官方账号的模型已支持无缝衔接),是否继续关闭并清理代理?", "d766536c18e8e990": "插件运行时 {version} 已安装,可以开始使用插件。", "d7e266bdc8064193": "分组名称", + "d896c62fb6712bda": "启用 {model}", "d8c47e9776cf1082": "主菜单", "d8c589c455675b46": "提示词设置已保存", "da521d1c1cbd36af": "需要授权安装证书", @@ -392,6 +395,7 @@ "ee6b89a6a740a4c4": "端口被占用时会自动选择新的随机端口并保存。修改后需要重启软件才会生效。", "f4694c46b1e19602": "最终请求类型", "f4dcb6a3ceb32247": "第 {page} / {count} 页", + "f4f0ead1116b5b62": "启用", "f4fa9f31ea2ae58d": "过去一年的 Token 用量日历", "f50276449943286c": "结束时间", "f69273dbbebfb3a1": "格式化", @@ -405,6 +409,7 @@ "f9aa11dbb15ce647": "周六", "f9b55ca75425161b": "未记录响应内容,请开启详细记录后重试。", "fa5b4b8a751c7d1b": "Cursor 使用的本地代理端口;填写 0 时启动时随机选择。", + "fac2a67ad87807c4": "确定", "fad86bf65f72c747": "下载进度", "fb11aa6f29827095": "检查中…", "fbe8778fa8b9bab5": "需要先初始化本地 CA", diff --git a/apps/desktop/src/shared/api.ts b/apps/desktop/src/shared/api.ts index 2c2d5cf..96eb648 100644 --- a/apps/desktop/src/shared/api.ts +++ b/apps/desktop/src/shared/api.ts @@ -249,6 +249,7 @@ export interface PluginModelDescriptor { providerType: string; maxOutputTokens: number | null; images: boolean; + enabled: boolean; } export interface PluginProviderDescriptor { @@ -474,6 +475,7 @@ export const api = { refreshPluginResource: (pluginId: string, resourceType: string, resourceId: string) => request(`/plugins/${encodeURIComponent(pluginId)}/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/refresh`, { method: "POST" }), deletePluginResource: (pluginId: string, resourceType: string, resourceId: string) => request(`/plugins/${encodeURIComponent(pluginId)}/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}`, { method: "DELETE" }), syncPluginModels: (pluginId: string, providerId: string) => request<{ models: number }>(`/plugins/${encodeURIComponent(pluginId)}/providers/${encodeURIComponent(providerId)}/models/sync`, { method: "POST" }), + setPluginModelEnabled: (pluginId: string, providerId: string, modelId: string, enabled: boolean) => request(`/plugins/${encodeURIComponent(pluginId)}/providers/${encodeURIComponent(providerId)}/models/enabled`, { method: "PUT", body: JSON.stringify({ modelId, enabled }) }), pluginResourceExportUrl: (servicePort: number, pluginId: string, resourceType: string) => `http://127.0.0.1:${servicePort}${API_ROOT}/plugins/${encodeURIComponent(pluginId)}/resources/${encodeURIComponent(resourceType)}/export`, removePluginConfiguration: (pluginId: string) => request(`/plugins/${encodeURIComponent(pluginId)}`, { method: "DELETE" }), pluginRuntime: () => request("/plugins/runtime"), diff --git a/apps/desktop/src/shared/ui/Tooltip.module.scss b/apps/desktop/src/shared/ui/Tooltip.module.scss index 4f2099a..7150b1f 100644 --- a/apps/desktop/src/shared/ui/Tooltip.module.scss +++ b/apps/desktop/src/shared/ui/Tooltip.module.scss @@ -4,6 +4,8 @@ position: fixed; z-index: 15000; max-width: 420px; + max-height: calc(100vh - 24px); + overflow-y: auto; padding: 8px 10px; color: var(--vscode-editorHoverWidget-foreground, var(--vscode-foreground)); background: var(--vscode-editorHoverWidget-background); @@ -13,4 +15,24 @@ font-size: type.$font-size-xs; line-height: 1.5; overflow-wrap: anywhere; + + scrollbar-width: thin; + scrollbar-color: var(--vscode-scrollbarSlider-background, rgba(0, 0, 0, 0.25)) transparent; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--vscode-scrollbarSlider-background, rgba(0, 0, 0, 0.25)); + border-radius: 999px; + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--vscode-scrollbarSlider-hoverBackground, rgba(0, 0, 0, 0.4)); + } } diff --git a/apps/desktop/src/shared/ui/Tooltip.tsx b/apps/desktop/src/shared/ui/Tooltip.tsx index eb0b088..0127f26 100644 --- a/apps/desktop/src/shared/ui/Tooltip.tsx +++ b/apps/desktop/src/shared/ui/Tooltip.tsx @@ -1,25 +1,92 @@ -import { autoUpdate, computePosition, flip, offset, shift, type VirtualElement } from "@floating-ui/dom"; -import { useLayoutEffect, useRef, useState, type ReactNode } from "react"; +import { autoUpdate, computePosition, flip, offset, shift, size, type VirtualElement } from "@floating-ui/dom"; +import { createContext, useContext, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { createPortal } from "react-dom"; import styles from "./Tooltip.module.scss"; export type TooltipAnchor = VirtualElement; -export function Tooltip({ id, anchor, children }: { id?: string; anchor: TooltipAnchor | null; children: ReactNode }) { +type TooltipContextValue = { + show: (anchor: TooltipAnchor, id: string | undefined, content: ReactNode) => void; + hide: () => void; +}; + +const TooltipContext = createContext(null); + +export function useTooltip() { + const context = useContext(TooltipContext); + if (!context) throw new Error("useTooltip must be used within TooltipProvider"); + return context; +} + +export function TooltipProvider({ children }: { children: ReactNode }) { + const [active, setActive] = useState<{ anchor: TooltipAnchor; id?: string; content: ReactNode } | null>(null); + const hideTimer = useRef | null>(null); + const clearHide = () => { + if (hideTimer.current !== null) { + window.clearTimeout(hideTimer.current); + hideTimer.current = null; + } + }; + const context = useMemo(() => ({ + show: (anchor: TooltipAnchor, id: string | undefined, content: ReactNode) => { + clearHide(); + setActive({ anchor, id, content }); + }, + hide: () => { + clearHide(); + hideTimer.current = window.setTimeout(() => { + setActive(null); + hideTimer.current = null; + }, 150); + }, + }), []); + + return + {children} + {active?.content} + ; +} + +export function Tooltip({ id, anchor, children, onPointerEnter, onPointerLeave }: { + id?: string; + anchor: TooltipAnchor | null; + children: ReactNode; + onPointerEnter?: () => void; + onPointerLeave?: () => void; +}) { const tooltipRef = useRef(null); const [position, setPosition] = useState<{ left: number; top: number } | null>(null); + const [availableHeight, setAvailableHeight] = useState(null); useLayoutEffect(() => { const tooltip = tooltipRef.current; if (!anchor || !tooltip) { setPosition(null); + setAvailableHeight(null); return; } + setPosition(null); + setAvailableHeight(null); const updatePosition = () => { void computePosition(anchor, tooltip, { placement: "top", - middleware: [offset(8), flip({ padding: 12 }), shift({ padding: 12 })], + middleware: [ + offset(8), + flip({ padding: 12 }), + shift({ padding: 12 }), + size({ + padding: 12, + apply({ availableHeight }) { + setAvailableHeight((current) => current === availableHeight ? current : availableHeight); + }, + }), + ], }).then(({ x, y }) => setPosition({ left: x, top: y })); }; @@ -35,7 +102,14 @@ export function Tooltip({ id, anchor, children }: { id?: string; anchor: Tooltip id={id} className={styles.root} role="tooltip" - style={{ left: position?.left ?? 0, top: position?.top ?? 0, visibility: position ? "visible" : "hidden" }} + style={{ + left: position?.left ?? 0, + top: position?.top ?? 0, + maxHeight: availableHeight ? `${availableHeight}px` : undefined, + visibility: position ? "visible" : "hidden", + }} + onPointerEnter={onPointerEnter} + onPointerLeave={onPointerLeave} > {children} , diff --git a/apps/desktop/src/shared/ui/TooltipTrigger.tsx b/apps/desktop/src/shared/ui/TooltipTrigger.tsx index 06d6aa6..57b9d6b 100644 --- a/apps/desktop/src/shared/ui/TooltipTrigger.tsx +++ b/apps/desktop/src/shared/ui/TooltipTrigger.tsx @@ -1,5 +1,6 @@ -import { cloneElement, useEffect, useId, useState, type FocusEventHandler, type PointerEventHandler, type ReactElement } from "react"; -import { Tooltip, type TooltipAnchor } from "./Tooltip"; +import { cloneElement, useCallback, useEffect, useId, useRef, useState, type FocusEventHandler, type PointerEventHandler, type ReactElement } from "react"; +import { useTooltip, type TooltipAnchor } from "./Tooltip"; + function anchorFor(element: HTMLElement): TooltipAnchor { return { contextElement: element, getBoundingClientRect: () => element.getBoundingClientRect() }; @@ -15,43 +16,71 @@ type TriggerProps = { }; export function TooltipTrigger({ label, children }: { label: string; children: ReactElement }) { + const { show: showTooltip, hide: hideTooltip } = useTooltip(); const [anchor, setAnchor] = useState(null); const tooltipId = useId(); + const timerRef = useRef | null>(null); + + const clearTimer = useCallback(() => { + if (timerRef.current !== null) { + window.clearTimeout(timerRef.current); + timerRef.current = null; + } + }, []); + const hide = useCallback(() => { + clearTimer(); + setAnchor(null); + hideTooltip(); + }, [clearTimer, hideTooltip]); + const scheduleHide = useCallback(() => { + clearTimer(); + setAnchor(null); + hideTooltip(); + }, [clearTimer, hideTooltip]); + + useEffect(() => () => clearTimer(), [clearTimer]); useEffect(() => { if (!anchor) return; - const close = (event: KeyboardEvent) => { if (event.key === "Escape") setAnchor(null); }; + const close = (event: KeyboardEvent) => { if (event.key === "Escape") hide(); }; document.addEventListener("keydown", close); return () => document.removeEventListener("keydown", close); - }, [anchor]); + }, [anchor, hide]); + const trigger = cloneElement(children, { "aria-describedby": [children.props["aria-describedby"], anchor ? tooltipId : null].filter(Boolean).join(" ") || undefined, onPointerMove: (event) => { children.props.onPointerMove?.(event); - if (event.pointerType !== "touch" && event.buttons === 0) { - const element = event.currentTarget; - setAnchor((current) => current ?? anchorFor(element)); + if (event.pointerType !== "touch" && event.buttons === 0 && !anchor) { + const nextAnchor = anchorFor(event.currentTarget); + setAnchor(nextAnchor); + showTooltip(nextAnchor, tooltipId, label); } }, onPointerLeave: (event) => { children.props.onPointerLeave?.(event); - setAnchor(null); + scheduleHide(); }, onPointerDown: (event) => { children.props.onPointerDown?.(event); - setAnchor(null); + hide(); }, onFocus: (event) => { children.props.onFocus?.(event); - setAnchor(event.currentTarget.matches(":focus-visible") ? anchorFor(event.currentTarget) : null); + clearTimer(); + if (event.currentTarget.matches(":focus-visible")) { + const nextAnchor = anchorFor(event.currentTarget); + setAnchor(nextAnchor); + showTooltip(nextAnchor, tooltipId, label); + } else { + setAnchor(null); + } }, onBlur: (event) => { children.props.onBlur?.(event); - setAnchor(null); + scheduleHide(); }, }); - return <> - {trigger} - {label} - ; + return trigger; } + diff --git a/server/plugins/build-in/antigravity-auth/oauth.ts b/server/plugins/build-in/antigravity-auth/oauth.ts index e0b6e20..4c04906 100644 --- a/server/plugins/build-in/antigravity-auth/oauth.ts +++ b/server/plugins/build-in/antigravity-auth/oauth.ts @@ -13,9 +13,6 @@ import { SCOPES, } from "./google_oauth.ts"; -export const CALLBACK_PORT = 51121; -export const CALLBACK_PATH = "/oauth-callback"; - const AUTHORIZATION_LIFETIME_MS = 5 * 60 * 1000; type Session = { createdAtMs: number }; @@ -151,7 +148,6 @@ export const antigravityAuthorizationCodeOAuth: OAuth2AuthorizationCodeAddMethod "en-US": "Authorize Antigravity with your Google Account for Gemini and Claude models.", "zh-CN": "使用 Google 账号完成 Antigravity 授权,以使用 Gemini 与 Claude 模型。", }, - callback: { port: CALLBACK_PORT, path: CALLBACK_PATH }, begin, complete, }; diff --git a/server/plugins/build-in/antigravity-auth/oauth_test.ts b/server/plugins/build-in/antigravity-auth/oauth_test.ts index 5be3ff6..9f51e68 100644 --- a/server/plugins/build-in/antigravity-auth/oauth_test.ts +++ b/server/plugins/build-in/antigravity-auth/oauth_test.ts @@ -33,6 +33,10 @@ function context(requests: Array<{ url: string; body?: string }>): PluginContext } Deno.test("authorization URL uses Core-owned state, callback, and PKCE challenge", async () => { + assert( + antigravityAuthorizationCodeOAuth.callback?.port === undefined, + "Antigravity must let Core allocate an available loopback port", + ); const result = await antigravityAuthorizationCodeOAuth.begin( { redirectUri: "http://127.0.0.1:51121/oauth-callback", diff --git a/server/plugins/build-in/antigravity-auth/plugin.json b/server/plugins/build-in/antigravity-auth/plugin.json index 32354f0..b43d0a3 100644 --- a/server/plugins/build-in/antigravity-auth/plugin.json +++ b/server/plugins/build-in/antigravity-auth/plugin.json @@ -2,8 +2,8 @@ "apiVersion": 1, "id": "dev.cursorbyok.plugins.antigravity-auth", "name": "Antigravity", - "version": "0.3.0", - "author": "Antigravity", + "version": "0.3.1", + "author": "@masudranaxpert", "minAppVersion": "0.1.0", "icon": "assets/antigravity.svg", "entry": "main.ts", diff --git a/server/src/control/mod.rs b/server/src/control/mod.rs index a8cb294..ae269de 100644 --- a/server/src/control/mod.rs +++ b/server/src/control/mod.rs @@ -177,6 +177,10 @@ pub fn api_router(service: ControlService) -> Router { "/__byok-api__/api/plugins/{plugin_id}/providers/{provider_id}/models/sync", post(plugins::sync_models), ) + .route( + "/__byok-api__/api/plugins/{plugin_id}/providers/{provider_id}/models/enabled", + put(plugins::set_model_enabled), + ) .route( "/__byok-api__/api/settings/observability", get(settings::get).put(settings::update), diff --git a/server/src/control/plugins.rs b/server/src/control/plugins.rs index 936ce55..ffae869 100644 --- a/server/src/control/plugins.rs +++ b/server/src/control/plugins.rs @@ -105,6 +105,25 @@ pub async fn sync_models( Ok(Json(serde_json::json!({ "models": count }))) } +pub async fn set_model_enabled( + State(service): State, + Path((plugin_id, provider_id)): Path<(String, String)>, + Json(input): Json, +) -> Result { + let model_id = input + .get("modelId") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| crate::Error::Config("modelId must be a string".into()))?; + let enabled = input + .get("enabled") + .and_then(serde_json::Value::as_bool) + .ok_or_else(|| crate::Error::Config("enabled must be a boolean".into()))?; + service + .plugin_set_model_enabled(&plugin_id, &provider_id, model_id, enabled) + .await?; + Ok(StatusCode::NO_CONTENT) +} + pub async fn runtime_status( State(service): State, ) -> Result> { diff --git a/server/src/control/service.rs b/server/src/control/service.rs index d6a7163..9d53581 100644 --- a/server/src/control/service.rs +++ b/server/src/control/service.rs @@ -241,6 +241,18 @@ impl ControlService { self.plugins.sync_models(plugin_id, provider_id).await } + pub async fn plugin_set_model_enabled( + &self, + plugin_id: &str, + provider_id: &str, + model_id: &str, + enabled: bool, + ) -> Result<()> { + self.plugins + .set_model_enabled(plugin_id, provider_id, model_id, enabled) + .await + } + pub async fn remove_plugin_configuration(&self, plugin_id: &str) -> Result<()> { self.plugins.remove(plugin_id).await } diff --git a/server/src/cursor/services/commit_message.rs b/server/src/cursor/services/commit_message.rs index f37043b..6198848 100644 --- a/server/src/cursor/services/commit_message.rs +++ b/server/src/cursor/services/commit_message.rs @@ -41,6 +41,7 @@ const DIFF_SINGLE_LIMIT: usize = 16_000; const PREVIOUS_COMMIT_LIMIT: usize = 12; const EXPLICIT_CONTEXT_LIMIT: usize = 20_000; const GENERATION_TIMEOUT: Duration = Duration::from_secs(180); +const COMMIT_MAX_OUTPUT_TOKENS: u64 = 30_000; pub async fn write_git_commit_message( State(registry): State, @@ -142,7 +143,10 @@ fn build_invocation( instructions: settings.effective_prompt().to_owned(), tools: Vec::new(), }, - model: ModelSpec::new(model_id.to_owned()), + model: ModelSpec { + max_output_tokens: Some(COMMIT_MAX_OUTPUT_TOKENS), + ..ModelSpec::new(model_id.to_owned()) + }, history: vec![ProjectedMessage { message_id: "commit-message".into(), role: Role::User, diff --git a/server/src/cursor/services/model_catalog.rs b/server/src/cursor/services/model_catalog.rs index b63e4f3..b9dbd08 100644 --- a/server/src/cursor/services/model_catalog.rs +++ b/server/src/cursor/services/model_catalog.rs @@ -831,6 +831,7 @@ mod tests { provider_type: "test".into(), max_output_tokens: None, images: false, + enabled: true, }); assert_eq!(details.model_id, "plugin:test/provider/model"); let agent::model_details::Credentials::ApiKeyCredentials(credentials) = diff --git a/server/src/plugin/descriptor.rs b/server/src/plugin/descriptor.rs index 2762255..ba92bc3 100644 --- a/server/src/plugin/descriptor.rs +++ b/server/src/plugin/descriptor.rs @@ -121,6 +121,7 @@ pub struct PluginModelDescriptor { pub provider_type: String, pub max_output_tokens: Option, pub images: bool, + pub enabled: bool, } #[derive(Clone, Debug, Serialize)] @@ -221,6 +222,7 @@ impl PluginModelDescriptor { provider_type: provider.provider_type.clone(), max_output_tokens: model.max_output_tokens, images: model.images, + enabled: model.enabled, } } } diff --git a/server/src/plugin/registry.rs b/server/src/plugin/registry.rs index c81cb29..0dc365c 100644 --- a/server/src/plugin/registry.rs +++ b/server/src/plugin/registry.rs @@ -189,7 +189,7 @@ impl PluginRegistry { .models(&entry.manifest.id, &provider.id) .await .unwrap_or_default(); - models.extend(stored.iter().map(|model| { + models.extend(stored.iter().filter(|model| model.enabled).map(|model| { PluginModelDescriptor::new( &entry.manifest.id, &entry.manifest.name, @@ -326,6 +326,13 @@ impl PluginRegistry { "plugin '{plugin_id}' does not define OAuth method '{method_id}'" )) })?; + // 同一添加入口只有一个活跃生命周期。重新开始时先丢弃旧会话, + // Drop 授权码会话中的 CallbackHandle 会立即释放 loopback listener。 + self.inner.oauth_sessions.lock().await.retain(|_, session| { + session.plugin_id != plugin_id + || session.resource_type != resource_type + || session.method_id != method_id + }); let worker = self.worker(&entry, &executable).await; let session_id = uuid::Uuid::new_v4().to_string(); @@ -876,6 +883,27 @@ impl PluginRegistry { .await } + pub async fn set_model_enabled( + &self, + plugin_id: &str, + provider_id: &str, + model_id: &str, + enabled: bool, + ) -> Result<()> { + let executable = self.executable()?; + let entry = self.find_entry(&executable, plugin_id).await?; + let provider = find_provider(&entry, provider_id)?; + if !provider.has_models { + return Err(Error::Config(format!( + "plugin provider '{provider_id}' does not enumerate models" + ))); + } + self.inner + .state + .set_model_enabled(plugin_id, provider_id, model_id, enabled) + .await + } + pub async fn remove(&self, plugin_id: &str) -> Result<()> { if let Some(worker) = self.inner.workers.lock().await.remove(plugin_id) { worker.stop().await; diff --git a/server/src/plugin/state.rs b/server/src/plugin/state.rs index f344961..74e9fb3 100644 --- a/server/src/plugin/state.rs +++ b/server/src/plugin/state.rs @@ -138,10 +138,16 @@ pub struct StoredModel { pub max_output_tokens: Option, #[serde(default)] pub images: bool, + #[serde(default = "default_model_enabled")] + pub enabled: bool, #[serde(default)] pub private_data: serde_json::Value, } +fn default_model_enabled() -> bool { + true +} + impl StoredModel { pub fn from_definition(value: &serde_json::Value) -> Result { let object = value @@ -179,6 +185,7 @@ impl StoredModel { .get("maxOutputTokens") .and_then(serde_json::Value::as_u64), images: capability("images"), + enabled: true, private_data: object .get("privateData") .cloned() @@ -328,6 +335,39 @@ impl PluginStateStore { provider_id: &str, models: &[StoredModel], ) -> Result<()> { + let previous = self.models(plugin_id, provider_id).await?; + let models = models + .iter() + .cloned() + .map(|mut model| { + if let Some(old) = previous.iter().find(|old| old.id == model.id) { + model.enabled = old.enabled; + } + model + }) + .collect::>(); + self.data + .update( + plugin_id, + &model_key(provider_id), + &serde_json::to_value(models)?, + ) + .await + } + + pub async fn set_model_enabled( + &self, + plugin_id: &str, + provider_id: &str, + model_id: &str, + enabled: bool, + ) -> Result<()> { + let mut models = self.models(plugin_id, provider_id).await?; + let model = models + .iter_mut() + .find(|model| model.id == model_id) + .ok_or_else(|| Error::RunNotFound(format!("plugin model {model_id}")))?; + model.enabled = enabled; self.data .update( plugin_id,