mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-03 18:23:51 +08:00
feat(antigravity): add Google Antigravity auth plugin with auto-rotation, model catalog and multi-turn support
This commit is contained in:
@@ -6,7 +6,7 @@ publish = false
|
||||
|
||||
[lib]
|
||||
name = "cursor_byok_desktop"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
@@ -99,11 +99,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 7px;
|
||||
gap: 6px;
|
||||
|
||||
> button {
|
||||
min-width: 0;
|
||||
flex: 0 1 auto;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { api, pluginText, type PluginDescriptor, type PluginImportFile, type PluginRuntimePhase, type PluginRuntimeStatus } from "../../shared/api";
|
||||
import { api, getDisabledPluginModelIds, pluginText, type PluginDescriptor, type PluginImportFile, type PluginRuntimePhase, type PluginRuntimeStatus } from "../../shared/api";
|
||||
import { useI18n } from "../../i18n/store";
|
||||
import { PageContent } from "../../shell/layout/PageContent";
|
||||
import { appStore, useAppStore } from "../../shared/store/appStore";
|
||||
@@ -138,7 +138,17 @@ function PluginCard({ plugin, onOpen }: {
|
||||
const [importing, setImporting] = useState(false);
|
||||
const configured = plugin.providers.some((provider) => provider.configured);
|
||||
const accountCount = plugin.resources.reduce((count, resource) => count + resource.resources.length, 0);
|
||||
const modelCount = plugin.providers.reduce((count, provider) => count + provider.models.length, 0);
|
||||
const [disabledModelIds, setDisabledModelIds] = useState<Set<string>>(() => getDisabledPluginModelIds());
|
||||
useEffect(() => {
|
||||
const handleUpdate = () => setDisabledModelIds(getDisabledPluginModelIds());
|
||||
window.addEventListener("cursor_plugin_models_changed", handleUpdate);
|
||||
return () => window.removeEventListener("cursor_plugin_models_changed", handleUpdate);
|
||||
}, []);
|
||||
|
||||
const modelCount = plugin.providers.reduce(
|
||||
(count, provider) => count + provider.models.filter((m) => !disabledModelIds.has(m.id)).length,
|
||||
0,
|
||||
);
|
||||
const subtitle = plugin.providers.map((provider) => pluginText(provider.displayName, locale)).join(" · ") || plugin.id;
|
||||
const importResource = plugin.resources.find((resource) => resource.import);
|
||||
const exportResource = plugin.resources.find((resource) => resource.resources.length > 0);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
|
||||
> span {
|
||||
>span {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: type.$font-size-xs;
|
||||
}
|
||||
@@ -37,6 +37,10 @@
|
||||
}
|
||||
|
||||
button {
|
||||
max-width: 240px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 6px 10px;
|
||||
color: var(--vscode-foreground);
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
@@ -97,15 +101,21 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.providerRow,
|
||||
.resourceRow {
|
||||
.providerCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.providerRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
|
||||
> div:first-child {
|
||||
>div:first-child {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -118,6 +128,576 @@
|
||||
}
|
||||
}
|
||||
|
||||
.resourceRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
|
||||
.resourceText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.resourceHeaderLine {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.resourceDescription {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: type.$font-size-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metricsRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.metricBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
.metricPrefix {
|
||||
font-weight: 700;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.metricValue {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
.claudeMetricBadge {
|
||||
background: rgba(234, 88, 12, 0.14);
|
||||
color: #fb923c;
|
||||
border: 1px solid rgba(234, 88, 12, 0.28);
|
||||
}
|
||||
|
||||
.geminiMetricBadge {
|
||||
background: rgba(59, 130, 246, 0.14);
|
||||
color: #60a5fa;
|
||||
border: 1px solid rgba(59, 130, 246, 0.28);
|
||||
}
|
||||
|
||||
.genericMetricBadge {
|
||||
background: color-mix(in srgb, var(--vscode-badge-background, #333) 40%, transparent);
|
||||
color: var(--vscode-badge-foreground, #ccc);
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-sideBar-border, #444) 40%, transparent);
|
||||
}
|
||||
|
||||
.proBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(135deg, rgba(234, 88, 12, 0.25), rgba(249, 115, 22, 0.15));
|
||||
color: #fb923c;
|
||||
border: 1px solid rgba(249, 115, 22, 0.4);
|
||||
box-shadow: 0 0 6px rgba(249, 115, 22, 0.15);
|
||||
}
|
||||
|
||||
.freeBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 5px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--vscode-badge-background, #333) 30%, transparent);
|
||||
color: var(--vscode-descriptionForeground, #888);
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-sideBar-border, #444) 40%, transparent);
|
||||
}
|
||||
|
||||
.providerActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modelsView {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modelsViewHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 2px;
|
||||
|
||||
.backButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
background: transparent;
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-sideBar-border) 60%, transparent);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
background: color-mix(in srgb, var(--vscode-foreground) 8%, transparent);
|
||||
border-color: var(--vscode-focusBorder, #007fd4);
|
||||
}
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
strong {
|
||||
color: var(--vscode-foreground);
|
||||
font-size: type.$font-size-base;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: type.$font-size-xs;
|
||||
}
|
||||
|
||||
.headerCountChip {
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-focusBorder, #007fd4);
|
||||
background: color-mix(in srgb, var(--vscode-focusBorder, #007fd4) 14%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-focusBorder, #007fd4) 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modelListToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
.searchIcon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.clearSearchBtn {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
background: color-mix(in srgb, var(--vscode-foreground) 10%, transparent);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
background: color-mix(in srgb, var(--vscode-foreground) 20%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modelSearchInput {
|
||||
width: 100%;
|
||||
padding: 7px 30px 7px 32px;
|
||||
font-size: type.$font-size-xs;
|
||||
color: var(--vscode-foreground);
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border, var(--vscode-sideBar-border));
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: color-mix(in srgb, var(--vscode-descriptionForeground) 70%, transparent);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--vscode-focusBorder, #007fd4);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--vscode-focusBorder, #007fd4) 20%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.modelBatchActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.pillButton {
|
||||
padding: 5px 12px;
|
||||
font-size: type.$font-size-xs;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
background: color-mix(in srgb, var(--vscode-button-secondaryBackground, #3a3d41) 60%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-sideBar-border) 80%, transparent);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-button-foreground, #fff);
|
||||
background: var(--vscode-button-secondaryHoverBackground, #45494e);
|
||||
border-color: color-mix(in srgb, var(--vscode-button-secondaryHoverBackground, #45494e) 50%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modelsViewList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.emptySearch {
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: type.$font-size-xs;
|
||||
}
|
||||
|
||||
.modelItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 9px 14px;
|
||||
background: color-mix(in srgb, var(--vscode-sideBar-background) 50%, var(--vscode-editor-background));
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-sideBar-border) 70%, transparent);
|
||||
border-radius: 8px;
|
||||
font-size: type.$font-size-xs;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
transition: all 0.18s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in srgb, var(--vscode-focusBorder, #007fd4) 50%, transparent);
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.18);
|
||||
background: color-mix(in srgb, var(--vscode-sideBar-background) 80%, var(--vscode-focusBorder, #007fd4) 6%);
|
||||
}
|
||||
}
|
||||
|
||||
.modelItemActive {
|
||||
border-color: color-mix(in srgb, var(--vscode-focusBorder, #007fd4) 55%, transparent);
|
||||
background: color-mix(in srgb, var(--vscode-focusBorder, #007fd4) 7%, var(--vscode-editor-background));
|
||||
}
|
||||
|
||||
.modelItemDisabled {
|
||||
opacity: 0.45;
|
||||
background: color-mix(in srgb, var(--vscode-editor-background) 80%, transparent);
|
||||
border-color: color-mix(in srgb, var(--vscode-sideBar-border) 40%, transparent);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modelCheckbox {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
accent-color: var(--vscode-focusBorder, #007fd4);
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.modelInfo {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.modelName {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modelId {
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: type.$font-size-2xs;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modelBadges {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.claudeTag,
|
||||
.geminiTag,
|
||||
.gptTag,
|
||||
.thinkingTag,
|
||||
.highTag,
|
||||
.mediumTag,
|
||||
.lowTag,
|
||||
.extraLowTag,
|
||||
.imageTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.claudeTag {
|
||||
color: #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
border: 1px solid rgba(245, 158, 11, 0.35);
|
||||
}
|
||||
|
||||
.geminiTag {
|
||||
color: #60a5fa;
|
||||
background: rgba(96, 165, 250, 0.12);
|
||||
border: 1px solid rgba(96, 165, 250, 0.35);
|
||||
}
|
||||
|
||||
.gptTag {
|
||||
color: #34d399;
|
||||
background: rgba(52, 211, 153, 0.12);
|
||||
border: 1px solid rgba(52, 211, 153, 0.35);
|
||||
}
|
||||
|
||||
.thinkingTag {
|
||||
color: #c084fc;
|
||||
background: rgba(192, 132, 252, 0.12);
|
||||
border: 1px solid rgba(192, 132, 252, 0.35);
|
||||
}
|
||||
|
||||
.highTag {
|
||||
color: #f87171;
|
||||
background: rgba(248, 113, 113, 0.12);
|
||||
border: 1px solid rgba(248, 113, 113, 0.35);
|
||||
}
|
||||
|
||||
.mediumTag {
|
||||
color: #fbbf24;
|
||||
background: rgba(251, 191, 36, 0.12);
|
||||
border: 1px solid rgba(251, 191, 36, 0.35);
|
||||
}
|
||||
|
||||
.lowTag {
|
||||
color: #2dd4bf;
|
||||
background: rgba(45, 212, 191, 0.12);
|
||||
border: 1px solid rgba(45, 212, 191, 0.35);
|
||||
}
|
||||
|
||||
.extraLowTag {
|
||||
color: #38bdf8;
|
||||
background: rgba(56, 189, 248, 0.12);
|
||||
border: 1px solid rgba(56, 189, 248, 0.35);
|
||||
}
|
||||
|
||||
.imageTag {
|
||||
color: #f472b6;
|
||||
background: rgba(244, 114, 182, 0.12);
|
||||
border: 1px solid rgba(244, 114, 182, 0.35);
|
||||
}
|
||||
|
||||
.resourceIdentity {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.resourceText {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.resourceRowDisabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.resourceActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.actionIconButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
color: var(--vscode-textLink-foreground, #3b82f6);
|
||||
background: color-mix(in srgb, var(--vscode-textLink-foreground, #3b82f6) 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-textLink-foreground, #3b82f6) 30%, transparent);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #fff;
|
||||
background: var(--vscode-textLink-foreground, #3b82f6);
|
||||
border-color: var(--vscode-textLink-foreground, #3b82f6);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.refreshingSpin {
|
||||
animation: spin 0.8s linear infinite;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.actionDeleteButton {
|
||||
color: #ef4444;
|
||||
background: color-mix(in srgb, #ef4444 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, #ef4444 30%, transparent);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #fff;
|
||||
background: #ef4444;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.readyBadge,
|
||||
.coolingBadge,
|
||||
.invalidBadge,
|
||||
.disabledBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: type.$font-size-2xs;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
.badgeDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.readyBadge {
|
||||
color: var(--vscode-testing-iconPassed, #73c991);
|
||||
background: color-mix(in srgb, var(--vscode-testing-iconPassed, #73c991) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-testing-iconPassed, #73c991) 30%, transparent);
|
||||
|
||||
.badgeDot {
|
||||
background: var(--vscode-testing-iconPassed, #73c991);
|
||||
box-shadow: 0 0 6px color-mix(in srgb, var(--vscode-testing-iconPassed, #73c991) 60%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.coolingBadge {
|
||||
color: var(--vscode-editorWarning-foreground, #cca700);
|
||||
background: color-mix(in srgb, var(--vscode-editorWarning-foreground, #cca700) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-editorWarning-foreground, #cca700) 30%, transparent);
|
||||
|
||||
.badgeDot {
|
||||
background: var(--vscode-editorWarning-foreground, #cca700);
|
||||
}
|
||||
}
|
||||
|
||||
.invalidBadge {
|
||||
color: var(--vscode-errorForeground, #f48771);
|
||||
background: color-mix(in srgb, var(--vscode-errorForeground, #f48771) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-errorForeground, #f48771) 30%, transparent);
|
||||
|
||||
.badgeDot {
|
||||
background: var(--vscode-errorForeground, #f48771);
|
||||
}
|
||||
}
|
||||
|
||||
.disabledBadge {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
background: color-mix(in srgb, var(--vscode-descriptionForeground) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-sideBar-border) 80%, transparent);
|
||||
|
||||
.badgeDot {
|
||||
background: var(--vscode-descriptionForeground);
|
||||
}
|
||||
}
|
||||
|
||||
.ready {
|
||||
color: var(--vscode-testing-iconPassed, #73c991) !important;
|
||||
}
|
||||
@@ -153,4 +733,4 @@
|
||||
.error {
|
||||
color: var(--vscode-errorForeground, #f48771);
|
||||
font-size: type.$font-size-xs;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
api,
|
||||
getDisabledPluginAccountIds,
|
||||
getDisabledPluginModelIds,
|
||||
setPluginAccountEnabled,
|
||||
setPluginModelEnabled,
|
||||
setMultiplePluginModelsEnabled,
|
||||
pluginText,
|
||||
type PluginAddMethod,
|
||||
type PluginDescriptor,
|
||||
@@ -14,6 +19,10 @@ import { appStore } from "../../shared/store/appStore";
|
||||
import { Button } from "../../shared/ui/Button";
|
||||
import { Card } from "../../shared/ui/Card";
|
||||
import { FormField, TextInput } from "../../shared/ui/FormControls";
|
||||
import { Icon } from "../../shared/ui/Icon";
|
||||
import { refreshIcon, trashIcon } from "../../shared/ui/icons";
|
||||
import { Switch } from "../../shared/ui/Switch";
|
||||
import { TooltipTrigger } from "../../shared/ui/TooltipTrigger";
|
||||
import styles from "./PluginResourcePanels.module.scss";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
@@ -122,8 +131,18 @@ function OAuthMethodCard({ pluginId, resourceType, method, onConfigured }: {
|
||||
{method.description && <span>{pluginText(method.description, locale)}</span>}
|
||||
{begun && status === "polling" && <div className={styles.deviceCode}>
|
||||
<small>{t("设备验证码")}</small>
|
||||
<button type="button" onClick={() => void copyCode(begun.userCode)}>{begun.userCode}</button>
|
||||
<button type="button" className={styles.copy} onClick={() => void copyCode(begun.userCode)}>
|
||||
<button
|
||||
type="button"
|
||||
title={begun.verificationUrlComplete || begun.verificationUrl || begun.userCode}
|
||||
onClick={() => void copyCode(begun.verificationUrlComplete || begun.verificationUrl || begun.userCode)}
|
||||
>
|
||||
{begun.userCode.startsWith("http") ? t("授权链接") : begun.userCode}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.copy}
|
||||
onClick={() => void copyCode(begun.verificationUrlComplete || begun.verificationUrl || begun.userCode)}
|
||||
>
|
||||
{copied ? t("已复制") : t("复制")}
|
||||
</button>
|
||||
</div>}
|
||||
@@ -141,6 +160,7 @@ function OAuthMethodCard({ pluginId, resourceType, method, onConfigured }: {
|
||||
export function PluginSettingsPanel({ plugin }: { plugin: PluginDescriptor }) {
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeProviderId, setActiveProviderId] = useState<string | null>(null);
|
||||
|
||||
const run = async (key: string, task: () => Promise<void>) => {
|
||||
setBusy(key);
|
||||
@@ -155,6 +175,40 @@ export function PluginSettingsPanel({ plugin }: { plugin: PluginDescriptor }) {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const autoRefreshResources = async () => {
|
||||
let didRefresh = false;
|
||||
for (const res of plugin.resources) {
|
||||
if (!res.canRefresh) continue;
|
||||
for (const item of res.resources) {
|
||||
if (!active) return;
|
||||
// Auto-refresh if resource is marked invalid or missing metrics
|
||||
if (item.state.status === "invalid" || item.metrics.length === 0) {
|
||||
try {
|
||||
await api.refreshPluginResource(plugin.id, res.type, item.id);
|
||||
didRefresh = true;
|
||||
} catch {
|
||||
// ignore background refresh failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (active && didRefresh) {
|
||||
await appStore.refreshPlugins();
|
||||
}
|
||||
};
|
||||
void autoRefreshResources();
|
||||
return () => { active = false; };
|
||||
}, [plugin.id]);
|
||||
|
||||
if (activeProviderId) {
|
||||
const provider = plugin.providers.find((p) => p.id === activeProviderId);
|
||||
if (provider) {
|
||||
return <ProviderModelsView provider={provider} onBack={() => setActiveProviderId(null)} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <div className={styles.panel}>
|
||||
{plugin.providers.map((provider) => <ProviderRow
|
||||
key={provider.id}
|
||||
@@ -164,11 +218,12 @@ export function PluginSettingsPanel({ plugin }: { plugin: PluginDescriptor }) {
|
||||
onSync={() => void run(`sync:${provider.id}`, async () => {
|
||||
await api.syncPluginModels(plugin.id, provider.id);
|
||||
})}
|
||||
onViewModels={() => setActiveProviderId(provider.id)}
|
||||
/>)}
|
||||
{plugin.resources.map((resource) => <ResourceList
|
||||
key={resource.type}
|
||||
resource={resource}
|
||||
busy={busy !== null}
|
||||
busyKey={busy}
|
||||
onRefresh={(item) => void run(`refresh:${item.id}`, async () => {
|
||||
await api.refreshPluginResource(plugin.id, resource.type, item.id);
|
||||
})}
|
||||
@@ -180,33 +235,190 @@ export function PluginSettingsPanel({ plugin }: { plugin: PluginDescriptor }) {
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ProviderRow({ provider, busy, syncing, onSync }: {
|
||||
function ProviderRow({ provider, busy, syncing, onSync, onViewModels }: {
|
||||
provider: PluginProviderDescriptor;
|
||||
busy: boolean;
|
||||
syncing: boolean;
|
||||
onSync: () => void;
|
||||
onViewModels: () => void;
|
||||
}) {
|
||||
const { locale } = useI18n();
|
||||
return <Card className={styles.providerRow}>
|
||||
<div>
|
||||
<strong>{pluginText(provider.displayName, locale)}</strong>
|
||||
<span>
|
||||
{provider.providerType}
|
||||
{" · "}
|
||||
{provider.models.length > 0 ? t("{count} 个模型", { count: provider.models.length }) : t("尚未同步模型")}
|
||||
{" · "}
|
||||
{provider.configured ? t("可调用") : t("未就绪")}
|
||||
</span>
|
||||
</div>
|
||||
{provider.hasModels && <Button size="small" disabled={busy} onClick={onSync}>
|
||||
{syncing ? t("正在同步…") : t("同步模型")}
|
||||
</Button>}
|
||||
</Card>;
|
||||
const [disabledIds, setDisabledIds] = useState<Set<string>>(() => getDisabledPluginModelIds());
|
||||
|
||||
useEffect(() => {
|
||||
const handleUpdate = () => setDisabledIds(getDisabledPluginModelIds());
|
||||
window.addEventListener("cursor_plugin_models_changed", handleUpdate);
|
||||
return () => window.removeEventListener("cursor_plugin_models_changed", handleUpdate);
|
||||
}, []);
|
||||
|
||||
const enabledCount = provider.models.filter((m) => !disabledIds.has(m.id)).length;
|
||||
|
||||
return (
|
||||
<Card className={styles.providerCard}>
|
||||
<div className={styles.providerRow}>
|
||||
<div>
|
||||
<strong>{pluginText(provider.displayName, locale)}</strong>
|
||||
<span>
|
||||
{provider.providerType}
|
||||
{" · "}
|
||||
{provider.models.length > 0
|
||||
? `${enabledCount}/${t("{count} 个模型", { count: provider.models.length })}`
|
||||
: t("尚未同步模型")}
|
||||
{" · "}
|
||||
{provider.configured ? t("可调用") : t("未就绪")}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.providerActions}>
|
||||
{provider.models.length > 0 && (
|
||||
<Button size="small" onClick={onViewModels}>
|
||||
{t("查看模型")}
|
||||
</Button>
|
||||
)}
|
||||
{provider.hasModels && (
|
||||
<Button size="small" variant="primary" disabled={busy} onClick={onSync}>
|
||||
{syncing ? t("正在同步…") : t("同步模型")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ResourceList({ resource, busy, onRefresh, onDelete }: {
|
||||
function ProviderModelsView({ provider, onBack }: { provider: PluginProviderDescriptor; onBack: () => void }) {
|
||||
const { locale } = useI18n();
|
||||
const [search, setSearch] = useState("");
|
||||
const [disabledIds, setDisabledIds] = useState<Set<string>>(() => getDisabledPluginModelIds());
|
||||
|
||||
useEffect(() => {
|
||||
const handleUpdate = () => setDisabledIds(getDisabledPluginModelIds());
|
||||
window.addEventListener("cursor_plugin_models_changed", handleUpdate);
|
||||
return () => window.removeEventListener("cursor_plugin_models_changed", handleUpdate);
|
||||
}, []);
|
||||
|
||||
const toggleModel = (modelId: string) => {
|
||||
const isCurrentlyDisabled = disabledIds.has(modelId);
|
||||
setPluginModelEnabled(modelId, isCurrentlyDisabled);
|
||||
setDisabledIds(getDisabledPluginModelIds());
|
||||
};
|
||||
|
||||
const toggleAll = (enable: boolean) => {
|
||||
const target = filteredModels.length > 0 ? filteredModels : provider.models;
|
||||
setMultiplePluginModelsEnabled(target.map((m) => m.id), enable);
|
||||
setDisabledIds(getDisabledPluginModelIds());
|
||||
};
|
||||
|
||||
const enabledCount = provider.models.filter((m) => !disabledIds.has(m.id)).length;
|
||||
|
||||
const filteredModels = provider.models.filter((m) => {
|
||||
if (!search.trim()) return true;
|
||||
const q = search.toLowerCase();
|
||||
const shortId = m.id.split("/").pop() || m.id;
|
||||
return m.displayName.toLowerCase().includes(q) || shortId.toLowerCase().includes(q);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.modelsView}>
|
||||
<div className={styles.modelsViewHeader}>
|
||||
<TooltipTrigger label={t("返回")}>
|
||||
<button type="button" className={styles.backButton} onClick={onBack} aria-label={t("返回")}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<div className={styles.headerTitle}>
|
||||
<strong>{pluginText(provider.displayName, locale)}</strong>
|
||||
<span>{t("模型列表")}</span>
|
||||
<span className={styles.headerCountChip}>
|
||||
{enabledCount}/{provider.models.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modelListToolbar}>
|
||||
<div className={styles.searchContainer}>
|
||||
<svg className={styles.searchIcon} width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("搜索模型…")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className={styles.modelSearchInput}
|
||||
/>
|
||||
{search && (
|
||||
<button type="button" className={styles.clearSearchBtn} onClick={() => setSearch("")} aria-label="Clear">
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.modelBatchActions}>
|
||||
<button type="button" onClick={() => toggleAll(true)} className={styles.pillButton}>
|
||||
{t("全选")}
|
||||
</button>
|
||||
<button type="button" onClick={() => toggleAll(false)} className={styles.pillButton}>
|
||||
{t("全不选")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modelsViewList}>
|
||||
{filteredModels.map((m) => {
|
||||
const shortId = m.id.split("/").pop() || m.id;
|
||||
const nameLower = m.displayName.toLowerCase();
|
||||
const idLower = m.id.toLowerCase();
|
||||
const isEnabled = !disabledIds.has(m.id);
|
||||
|
||||
const isClaude = nameLower.includes("claude") || idLower.includes("claude");
|
||||
const isGemini = nameLower.includes("gemini") || idLower.includes("gemini");
|
||||
const isGpt = nameLower.includes("gpt") || idLower.includes("gpt");
|
||||
const isThinking = nameLower.includes("thinking") || idLower.includes("thinking");
|
||||
const isHigh = nameLower.includes("(high)") || idLower.includes("-high");
|
||||
const isMedium = nameLower.includes("(medium)") || idLower.includes("-medium");
|
||||
const isLow = nameLower.includes("(low)") || idLower.includes("-low");
|
||||
const isExtraLow = nameLower.includes("(extra-low)") || idLower.includes("-extra-low");
|
||||
const isImage = nameLower.includes("image") || idLower.includes("image");
|
||||
|
||||
return (
|
||||
<div
|
||||
key={m.id}
|
||||
className={`${styles.modelItem} ${isEnabled ? styles.modelItemActive : styles.modelItemDisabled}`}
|
||||
onClick={() => toggleModel(m.id)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isEnabled}
|
||||
onChange={() => toggleModel(m.id)}
|
||||
className={styles.modelCheckbox}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div className={styles.modelInfo}>
|
||||
<span className={styles.modelName}>{pluginText(m.displayName, locale) || shortId}</span>
|
||||
<span className={styles.modelId}>{shortId}</span>
|
||||
</div>
|
||||
<div className={styles.modelBadges}>
|
||||
{isClaude && <span className={styles.claudeTag}>Claude</span>}
|
||||
{isGemini && <span className={styles.geminiTag}>Gemini</span>}
|
||||
{isGpt && <span className={styles.gptTag}>GPT-OSS</span>}
|
||||
{isThinking && <span className={styles.thinkingTag}>Thinking</span>}
|
||||
{isHigh && <span className={styles.highTag}>High</span>}
|
||||
{isMedium && <span className={styles.mediumTag}>Medium</span>}
|
||||
{isLow && <span className={styles.lowTag}>Low</span>}
|
||||
{isExtraLow && <span className={styles.extraLowTag}>Extra-Low</span>}
|
||||
{isImage && <span className={styles.imageTag}>Image</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{filteredModels.length === 0 && (
|
||||
<div className={styles.emptySearch}>
|
||||
<span>{t("暂无数据")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ResourceList({ resource, busyKey, onRefresh, onDelete }: {
|
||||
resource: PluginResourceDescriptor;
|
||||
busy: boolean;
|
||||
busyKey: string | null;
|
||||
onRefresh: (item: PluginResourceView) => void;
|
||||
onDelete: (item: PluginResourceView) => void;
|
||||
}) {
|
||||
@@ -228,14 +440,20 @@ function ResourceList({ resource, busy, onRefresh, onDelete }: {
|
||||
<TextInput aria-label={t("搜索资源")} placeholder={t("搜索资源")} value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||
</div>}
|
||||
<div className={styles.resourceList}>
|
||||
{visible.map((item) => <ResourceRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
canRefresh={resource.canRefresh}
|
||||
disabled={busy}
|
||||
onRefresh={() => onRefresh(item)}
|
||||
onDelete={() => onDelete(item)}
|
||||
/>)}
|
||||
{visible.map((item) => {
|
||||
const isRefreshing = busyKey === `refresh:${item.id}`;
|
||||
const isDeleting = busyKey === `delete:${item.id}`;
|
||||
return <ResourceRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
canRefresh={resource.canRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
isDeleting={isDeleting}
|
||||
disabled={isRefreshing || isDeleting}
|
||||
onRefresh={() => onRefresh(item)}
|
||||
onDelete={() => onDelete(item)}
|
||||
/>;
|
||||
})}
|
||||
{visible.length === 0 && <span className={styles.empty}>{t("还没有资源,请先添加。")}</span>}
|
||||
</div>
|
||||
{pageCount > 1 && <div className={styles.pagination}>
|
||||
@@ -247,40 +465,116 @@ function ResourceList({ resource, busy, onRefresh, onDelete }: {
|
||||
</FormField>;
|
||||
}
|
||||
|
||||
function ResourceRow({ item, canRefresh, disabled, onRefresh, onDelete }: {
|
||||
function ResourceRow({ item, canRefresh, isRefreshing, isDeleting, disabled, onRefresh, onDelete }: {
|
||||
item: PluginResourceView;
|
||||
canRefresh: boolean;
|
||||
isRefreshing: boolean;
|
||||
isDeleting: boolean;
|
||||
disabled: boolean;
|
||||
onRefresh: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const { locale } = useI18n();
|
||||
return <Card className={styles.resourceRow}>
|
||||
<div>
|
||||
<strong>{item.displayName}</strong>
|
||||
{item.description && <span>{pluginText(item.description, locale)}</span>}
|
||||
{item.metrics.map((metric) => <span key={metric.id}>
|
||||
{metric.unit === "percent"
|
||||
? t("{label} 剩余 {percent}%", { label: pluginText(metric.label, locale), percent: Math.round(metric.value) })
|
||||
: `${pluginText(metric.label, locale)}: ${metric.value}`}
|
||||
</span>)}
|
||||
const [disabledAccountIds, setDisabledAccountIds] = useState<Set<string>>(() => getDisabledPluginAccountIds());
|
||||
|
||||
useEffect(() => {
|
||||
const handleUpdate = () => setDisabledAccountIds(getDisabledPluginAccountIds());
|
||||
window.addEventListener("cursor_plugin_accounts_changed", handleUpdate);
|
||||
return () => window.removeEventListener("cursor_plugin_accounts_changed", handleUpdate);
|
||||
}, []);
|
||||
|
||||
const isEnabled = !disabledAccountIds.has(item.id);
|
||||
|
||||
const toggleAccount = (checked: boolean) => {
|
||||
setPluginAccountEnabled(item.id, checked);
|
||||
setDisabledAccountIds(getDisabledPluginAccountIds());
|
||||
};
|
||||
|
||||
return <Card className={`${styles.resourceRow} ${!isEnabled ? styles.resourceRowDisabled : ""}`}>
|
||||
<div className={styles.resourceIdentity}>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
disabled={disabled}
|
||||
label={item.displayName}
|
||||
onChange={toggleAccount}
|
||||
/>
|
||||
<div className={styles.resourceText}>
|
||||
<div className={styles.resourceHeaderLine}>
|
||||
<strong>{item.displayName}</strong>
|
||||
{item.description && (() => {
|
||||
const desc = pluginText(item.description, locale).trim();
|
||||
const isPro = desc.toLowerCase().includes("pro") || desc.toLowerCase().includes("ultra") || desc.toLowerCase().includes("premium") || desc.toLowerCase().includes("advanced");
|
||||
const label = isPro ? (desc.toLowerCase().includes("ultra") ? "ULTRA" : "PRO") : "FREE";
|
||||
return <span className={isPro ? styles.proBadge : styles.freeBadge}>{isPro ? `🔥 ${label}` : label}</span>;
|
||||
})()}
|
||||
</div>
|
||||
{item.metrics.length > 0 && (
|
||||
<div className={styles.metricsRow}>
|
||||
{item.metrics.map((metric) => {
|
||||
const isClaude = metric.id.toLowerCase().includes("claude");
|
||||
const isGemini = metric.id.toLowerCase().includes("gemini");
|
||||
const badgeStyle = isClaude
|
||||
? styles.claudeMetricBadge
|
||||
: isGemini
|
||||
? styles.geminiMetricBadge
|
||||
: styles.genericMetricBadge;
|
||||
const shortLabel = isClaude ? "C" : isGemini ? "G" : pluginText(metric.label, locale);
|
||||
return (
|
||||
<span
|
||||
key={metric.id}
|
||||
className={`${styles.metricBadge} ${badgeStyle}`}
|
||||
title={metric.resetAtMs ? `${pluginText(metric.label, locale)} Reset: ${new Date(metric.resetAtMs).toLocaleTimeString()}` : `${pluginText(metric.label, locale)} Quota: ${Math.round(metric.value)}%`}
|
||||
>
|
||||
<span className={styles.metricPrefix}>{shortLabel}</span>
|
||||
<span className={styles.metricValue}>{Math.round(metric.value)}%</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<StateBadge state={item.state} />
|
||||
{canRefresh && <Button size="small" disabled={disabled} onClick={onRefresh}>{t("刷新")}</Button>}
|
||||
<Button size="small" disabled={disabled} onClick={onDelete}>{t("删除")}</Button>
|
||||
<div className={styles.resourceActions}>
|
||||
<StateBadge isEnabled={isEnabled} state={item.state} />
|
||||
{canRefresh && (
|
||||
<TooltipTrigger label={t("刷新")}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.actionIconButton} ${isRefreshing ? styles.refreshingSpin : ""}`}
|
||||
disabled={disabled}
|
||||
onClick={onRefresh}
|
||||
aria-label={t("刷新")}
|
||||
>
|
||||
<Icon icon={refreshIcon} size="1.05em" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
<TooltipTrigger label={t("删除")}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.actionIconButton} ${styles.actionDeleteButton}`}
|
||||
disabled={disabled || isDeleting}
|
||||
onClick={onDelete}
|
||||
aria-label={t("删除")}
|
||||
>
|
||||
<Icon icon={trashIcon} size="1.05em" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
</div>
|
||||
</Card>;
|
||||
}
|
||||
|
||||
function StateBadge({ state }: { state: PluginResourceView["state"] }) {
|
||||
function StateBadge({ isEnabled = true, state }: { isEnabled?: boolean; state: PluginResourceView["state"] }) {
|
||||
if (!isEnabled) {
|
||||
return <span className={styles.disabledBadge}><span className={styles.badgeDot} />{t("已停用")}</span>;
|
||||
}
|
||||
if (state.status === "cooling") {
|
||||
return <span className={styles.cooling} title={state.message ?? undefined}>{t("冷却中")}</span>;
|
||||
return <span className={styles.coolingBadge} title={state.message ?? undefined}><span className={styles.badgeDot} />{t("冷却中")}</span>;
|
||||
}
|
||||
if (state.status === "invalid") {
|
||||
return <span className={styles.invalid} title={state.message ?? undefined}>{t("已失效")}</span>;
|
||||
return <span className={styles.invalidBadge} title={state.message ?? undefined}><span className={styles.badgeDot} />{t("已失效")}</span>;
|
||||
}
|
||||
return <span className={styles.ready}>{t("可用")}</span>;
|
||||
return <span className={styles.readyBadge}><span className={styles.badgeDot} />{t("可用")}</span>;
|
||||
}
|
||||
|
||||
function errorText(cause: unknown) {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"0e41f8e3d59ec47b": "Storage management",
|
||||
"0e67021ebf0a3580": "Import complete: added {imported} models and skipped {skipped} existing models",
|
||||
"0ec1e85b0c3cfa65": "Call details",
|
||||
"0fa9ab1b5cdaca63": "Authorization URL",
|
||||
"105a9082c346f958": "Testing…",
|
||||
"124be3f86f197802": "Token usage",
|
||||
"12ae77e6202d063e": "Custom Headers",
|
||||
@@ -139,6 +140,7 @@
|
||||
"550eddc3c7fefa99": "Sponsored",
|
||||
"56432ba297009bdc": "Initialize the CA first",
|
||||
"56627c94a9decee6": "Maximum output tokens",
|
||||
"572cf45ba43634b3": "Back",
|
||||
"576d81bb0631b165": "Import",
|
||||
"5886afc1c71df1fe": "Shown in the Cursor model description.",
|
||||
"59346e82b3dd2998": "TAB service address",
|
||||
@@ -158,7 +160,7 @@
|
||||
"621f63a5f08384ac": "Cache read: {tokens}",
|
||||
"6320b4a8722a851f": "Status",
|
||||
"63c73c4730f4473e": "Apply",
|
||||
"63d90d977348ab1f": "Duplicate",
|
||||
"63d90d977348ab1f": "Copy",
|
||||
"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}",
|
||||
@@ -175,6 +177,7 @@
|
||||
"6ae80538c2b2572d": "Minimize window",
|
||||
"6d1876364ac6457d": "Proxy mode",
|
||||
"6e86570183c3cdd0": "You're up to date",
|
||||
"6f281e1d8380a055": "Hide models",
|
||||
"7005693f4f050bce": "Cache I/O {cost}",
|
||||
"72644ec4389da2f7": "Default layout",
|
||||
"736c9dc2a04c65fd": "The model configuration changed. Refresh and try again.",
|
||||
@@ -195,6 +198,7 @@
|
||||
"802b0faf0ceb513e": "{label}: {percent}% left",
|
||||
"80a57e03f0717f91": "Not configured",
|
||||
"811a3b22a5a7f2d5": "Unable to connect to the local management service",
|
||||
"8135034002c770ac": "View models",
|
||||
"8213941f12320ce1": "This operating system or CPU architecture is not currently supported",
|
||||
"83c4efccd9a6bf69": "Connectivity test cancelled: {successful} succeeded, {failed} failed",
|
||||
"83e8d0b7aff2b394": "Downloaded {downloaded} / {total}",
|
||||
@@ -214,12 +218,13 @@
|
||||
"8d0c47eb9eac2d34": "Call type",
|
||||
"8df48894086d6fbd": "Reason (optional)",
|
||||
"8e2d04638a11a7cb": "Only determines the request and response format; it does not change the request URL.",
|
||||
"8ec7b052f694e3c0": "Search models…",
|
||||
"8f6f8d979c981ced": "Copied",
|
||||
"8f9b0d6cc477d334": "Choose how Cursor connects to TAB endpoints.",
|
||||
"90800c48a1dd0655": "{label} must be a JSON object",
|
||||
"919cb0ce0c8db4e7": "Leave blank to keep the current password",
|
||||
"91aaf184cfc17ffd": "Overview",
|
||||
"91af6e57e7453fbe": "Add account",
|
||||
"91af6e57e7453fbe": "Add",
|
||||
"92156a483d4ba248": "Only request, response, and trace attachments are deleted; call summaries, metrics, and configuration are kept.",
|
||||
"940a168911ade998": "Items per page",
|
||||
"945fb1c67eca8493": "Installing the plugin runtime",
|
||||
@@ -259,6 +264,7 @@
|
||||
"a748cc074f78de00": "View details",
|
||||
"a7617f42f898b2bf": "Use complete request URL",
|
||||
"a8036485f9227f2c": "Drag to reorder",
|
||||
"a8c3698b5b8c485d": "Disabled",
|
||||
"a98585871c5313ff": "Display name",
|
||||
"ab9084a640fbb864": "Deselect all",
|
||||
"abecab6701177721": "Launch at login enabled",
|
||||
@@ -363,11 +369,12 @@
|
||||
"e828bd3a0151edc2": "The local CA must be trusted by the system",
|
||||
"e8b1268c1e3610f2": "Existing",
|
||||
"ea26b760e930a7ca": "Call observability",
|
||||
"ea4bf042d1bc5eb8": "Models",
|
||||
"eb11e2df1d8ae387": "Provider URL",
|
||||
"eb1be07f2ca6e506": "Estimated using Claude Opus 4.7 pricing.",
|
||||
"eb4a3db23661fb52": "Applies to every model in this group and is used as the badge label in Cursor's model picker; clear it to fall back to the server domain.",
|
||||
"eb77492c9f76a7e1": "The install command has been copied. Click “Open terminal”, paste it into the terminal, and enter your password when prompted.",
|
||||
"eba54690937bc532": "Manage accounts",
|
||||
"eba54690937bc532": "Manage",
|
||||
"ed31fbb483ee1b0a": "Actions",
|
||||
"edc70de18c6da1a6": "Install local CA",
|
||||
"ee239f3943293f87": "Sunday",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"0e41f8e3d59ec47b": "存储管理",
|
||||
"0e67021ebf0a3580": "导入完成:新增 {imported} 个模型,跳过 {skipped} 个已存在模型",
|
||||
"0ec1e85b0c3cfa65": "调用详情",
|
||||
"0fa9ab1b5cdaca63": "授权链接",
|
||||
"105a9082c346f958": "测试中…",
|
||||
"124be3f86f197802": "Token 消耗",
|
||||
"12ae77e6202d063e": "自定义 Headers",
|
||||
@@ -139,6 +140,7 @@
|
||||
"550eddc3c7fefa99": "推广",
|
||||
"56432ba297009bdc": "请先初始化 CA",
|
||||
"56627c94a9decee6": "最大输出 Token",
|
||||
"572cf45ba43634b3": "返回",
|
||||
"576d81bb0631b165": "导入",
|
||||
"5886afc1c71df1fe": "显示在 Cursor 模型说明中。",
|
||||
"59346e82b3dd2998": "TAB 服务地址",
|
||||
@@ -175,6 +177,7 @@
|
||||
"6ae80538c2b2572d": "最小化窗口",
|
||||
"6d1876364ac6457d": "代理方式",
|
||||
"6e86570183c3cdd0": "当前已是最新版本",
|
||||
"6f281e1d8380a055": "收起模型",
|
||||
"7005693f4f050bce": "缓存读写 {cost}",
|
||||
"72644ec4389da2f7": "默认平铺",
|
||||
"736c9dc2a04c65fd": "模型配置已发生变化,请刷新后重试",
|
||||
@@ -195,6 +198,7 @@
|
||||
"802b0faf0ceb513e": "{label} 剩余 {percent}%",
|
||||
"80a57e03f0717f91": "未配置",
|
||||
"811a3b22a5a7f2d5": "无法连接本地管理服务",
|
||||
"8135034002c770ac": "查看模型",
|
||||
"8213941f12320ce1": "当前操作系统或 CPU 架构暂不受支持",
|
||||
"83c4efccd9a6bf69": "连通性测试已取消:成功 {successful},失败 {failed}",
|
||||
"83e8d0b7aff2b394": "已下载 {downloaded} / {total}",
|
||||
@@ -214,6 +218,7 @@
|
||||
"8d0c47eb9eac2d34": "调用类型",
|
||||
"8df48894086d6fbd": "原因(可选)",
|
||||
"8e2d04638a11a7cb": "只决定请求与响应的格式,不会改变请求地址。",
|
||||
"8ec7b052f694e3c0": "搜索模型…",
|
||||
"8f6f8d979c981ced": "已复制",
|
||||
"8f9b0d6cc477d334": "控制 Cursor TAB 相关接口的连接方式。",
|
||||
"90800c48a1dd0655": "{label} 必须是 JSON 对象",
|
||||
@@ -259,6 +264,7 @@
|
||||
"a748cc074f78de00": "查看详情",
|
||||
"a7617f42f898b2bf": "使用完整请求地址",
|
||||
"a8036485f9227f2c": "拖动排序",
|
||||
"a8c3698b5b8c485d": "已停用",
|
||||
"a98585871c5313ff": "显示名称",
|
||||
"ab9084a640fbb864": "全不选",
|
||||
"abecab6701177721": "已开启开机启动",
|
||||
@@ -362,6 +368,7 @@
|
||||
"e825a2a42c22380e": "模型类型",
|
||||
"e828bd3a0151edc2": "需要在系统中信任本地 CA",
|
||||
"e8b1268c1e3610f2": "已存在",
|
||||
"ea4bf042d1bc5eb8": "模型列表",
|
||||
"ea26b760e930a7ca": "调用观测",
|
||||
"eb11e2df1d8ae387": "上游地址",
|
||||
"eb1be07f2ca6e506": "按 Claude Opus 4.7 价格估算。",
|
||||
|
||||
@@ -292,9 +292,83 @@ export type ConfiguredModel =
|
||||
| { kind: "builtin"; id: string; name: string; builtin: Model }
|
||||
| { kind: "plugin"; id: string; name: string; plugin: PluginModelDescriptor };
|
||||
|
||||
let cachedDisabledPluginModelIds: Set<string> = new Set();
|
||||
let cachedDisabledPluginAccountIds: Set<string> = new Set();
|
||||
let modelsLoaded = false;
|
||||
let accountsLoaded = false;
|
||||
|
||||
export function getDisabledPluginModelIds(): Set<string> {
|
||||
if (!modelsLoaded) {
|
||||
modelsLoaded = true;
|
||||
void api.disabledPluginModels().then((ids) => {
|
||||
cachedDisabledPluginModelIds = new Set(ids);
|
||||
window.dispatchEvent(new CustomEvent("cursor_plugin_models_changed"));
|
||||
}).catch(() => {});
|
||||
}
|
||||
return new Set(cachedDisabledPluginModelIds);
|
||||
}
|
||||
|
||||
export function setPluginModelEnabled(modelId: string, enabled: boolean): void {
|
||||
if (enabled) {
|
||||
cachedDisabledPluginModelIds.delete(modelId);
|
||||
} else {
|
||||
cachedDisabledPluginModelIds.add(modelId);
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent("cursor_plugin_models_changed"));
|
||||
void api.setDisabledPluginModels([...cachedDisabledPluginModelIds]).catch(() => {});
|
||||
}
|
||||
|
||||
export function setMultiplePluginModelsEnabled(modelIds: string[], enabled: boolean): void {
|
||||
if (enabled) {
|
||||
for (const id of modelIds) {
|
||||
cachedDisabledPluginModelIds.delete(id);
|
||||
}
|
||||
} else {
|
||||
for (const id of modelIds) {
|
||||
cachedDisabledPluginModelIds.add(id);
|
||||
}
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent("cursor_plugin_models_changed"));
|
||||
void api.setDisabledPluginModels([...cachedDisabledPluginModelIds]).catch(() => {});
|
||||
}
|
||||
|
||||
export function getDisabledPluginAccountIds(): Set<string> {
|
||||
if (!accountsLoaded) {
|
||||
accountsLoaded = true;
|
||||
void api.disabledPluginAccounts().then((ids) => {
|
||||
cachedDisabledPluginAccountIds = new Set(ids);
|
||||
window.dispatchEvent(new CustomEvent("cursor_plugin_accounts_changed"));
|
||||
}).catch(() => {});
|
||||
}
|
||||
return new Set(cachedDisabledPluginAccountIds);
|
||||
}
|
||||
|
||||
export function setPluginAccountEnabled(accountId: string, enabled: boolean): void {
|
||||
if (enabled) {
|
||||
cachedDisabledPluginAccountIds.delete(accountId);
|
||||
} else {
|
||||
cachedDisabledPluginAccountIds.add(accountId);
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent("cursor_plugin_accounts_changed"));
|
||||
void api.setDisabledPluginAccounts([...cachedDisabledPluginAccountIds]).catch(() => {});
|
||||
}
|
||||
|
||||
export function updateCachedDisabledStates(models: string[], accounts: string[]): void {
|
||||
cachedDisabledPluginModelIds = new Set(models);
|
||||
cachedDisabledPluginAccountIds = new Set(accounts);
|
||||
modelsLoaded = true;
|
||||
accountsLoaded = true;
|
||||
window.dispatchEvent(new CustomEvent("cursor_plugin_models_changed"));
|
||||
window.dispatchEvent(new CustomEvent("cursor_plugin_accounts_changed"));
|
||||
}
|
||||
|
||||
export function configuredPluginModels(plugins: PluginDescriptor[]): PluginModelDescriptor[] {
|
||||
const disabled = getDisabledPluginModelIds();
|
||||
return plugins.flatMap((plugin) =>
|
||||
plugin.providers.flatMap((provider) => provider.configured ? provider.models : []));
|
||||
plugin.providers.flatMap((provider) =>
|
||||
provider.configured ? provider.models.filter((m) => !disabled.has(m.id)) : []
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function configuredModels(models: Model[], plugins: PluginDescriptor[]): ConfiguredModel[] {
|
||||
@@ -466,6 +540,10 @@ export const api = {
|
||||
cursorHarness: () => request<CursorHarnessStatus>("/harness/cursor/status"),
|
||||
initializeCursorCa: () => request<CursorHarnessStatus>("/harness/cursor/ca/initialize", { method: "POST" }),
|
||||
plugins: () => request<PluginDescriptor[]>("/plugins"),
|
||||
disabledPluginModels: () => request<string[]>("/plugins/disabled-models"),
|
||||
setDisabledPluginModels: (modelIds: string[]) => request<string[]>("/plugins/disabled-models", { method: "PUT", body: JSON.stringify({ modelIds }) }),
|
||||
disabledPluginAccounts: () => request<string[]>("/plugins/disabled-accounts"),
|
||||
setDisabledPluginAccounts: (accountIds: string[]) => request<string[]>("/plugins/disabled-accounts", { method: "PUT", body: JSON.stringify({ accountIds }) }),
|
||||
pluginOAuthBegin: (pluginId: string, resourceType: string, methodId: string) => request<PluginOAuthBegin>(`/plugins/${encodeURIComponent(pluginId)}/resources/${encodeURIComponent(resourceType)}/add/${encodeURIComponent(methodId)}/begin`, { method: "POST" }),
|
||||
pluginOAuthPoll: (sessionId: string, signal?: AbortSignal) => request<PluginOAuthPoll>(`/plugins/oauth/${encodeURIComponent(sessionId)}/poll`, { method: "POST", signal }),
|
||||
importPluginResources: (pluginId: string, resourceType: string, files: PluginImportFile[]) => request<PluginImportResult>(`/plugins/${encodeURIComponent(pluginId)}/resources/${encodeURIComponent(resourceType)}/import`, { method: "POST", body: JSON.stringify(files) }),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { api, type CursorHarnessStatus, type LlmCall, type Model, type ModelInput, type Overview, type PluginDescriptor, type PluginRuntimeStatus, type PortSettings } from "../api";
|
||||
import { api, updateCachedDisabledStates, type CursorHarnessStatus, type LlmCall, type Model, type ModelInput, type Overview, type PluginDescriptor, type PluginRuntimeStatus, type PortSettings } from "../api";
|
||||
import { applyTheme, isThemeId, type ThemeId } from "../theme/theme";
|
||||
|
||||
export type AppSnapshot = {
|
||||
@@ -151,7 +151,13 @@ export const appStore = {
|
||||
},
|
||||
async refreshPlugins() {
|
||||
try {
|
||||
update({ plugins: await api.plugins() });
|
||||
const [plugins, disabledModels, disabledAccounts] = await Promise.all([
|
||||
api.plugins(),
|
||||
api.disabledPluginModels(),
|
||||
api.disabledPluginAccounts(),
|
||||
]);
|
||||
updateCachedDisabledStates(disabledModels, disabledAccounts);
|
||||
update({ plugins });
|
||||
} catch (cause) {
|
||||
update({ error: cause instanceof Error ? cause.message : String(cause) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user