mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-09 00:05:48 +08:00
feat(desktop): add promotion marquee and proxy fix
This commit is contained in:
Generated
+10
@@ -23,6 +23,7 @@
|
|||||||
"keepalive-for-react-router": "^5.0.7",
|
"keepalive-for-react-router": "^5.0.7",
|
||||||
"monaco-editor": "0.56.0",
|
"monaco-editor": "0.56.0",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
|
"react-css-marquee": "^1.0.1",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"react-router-dom": "^7.18.2",
|
"react-router-dom": "^7.18.2",
|
||||||
"sortablejs": "^1.15.7",
|
"sortablejs": "^1.15.7",
|
||||||
@@ -2978,6 +2979,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-css-marquee": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-css-marquee/-/react-css-marquee-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-jPHydXILF3oIP2gOvabfbEY3PUdbkHtDVA4mqYQiYvEdLWk9oqNJkUwDBxHK1RtltvQY3zkyuxTaGnw+G8x48w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.8",
|
"version": "19.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"keepalive-for-react-router": "^5.0.7",
|
"keepalive-for-react-router": "^5.0.7",
|
||||||
"monaco-editor": "0.56.0",
|
"monaco-editor": "0.56.0",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
|
"react-css-marquee": "^1.0.1",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"react-router-dom": "^7.18.2",
|
"react-router-dom": "^7.18.2",
|
||||||
"sortablejs": "^1.15.7",
|
"sortablejs": "^1.15.7",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [],
|
"windows": [],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "default-src 'self'; img-src 'self' asset: http://asset.localhost data: https:; style-src 'self' 'unsafe-inline'; connect-src 'self' http://127.0.0.1:*",
|
"csp": "default-src 'self'; img-src 'self' asset: http://asset.localhost data: https:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src 'self' http://127.0.0.1:*",
|
||||||
"dangerousDisableAssetCspModification": [
|
"dangerousDisableAssetCspModification": [
|
||||||
"style-src"
|
"style-src"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function installDemoApi() {
|
|||||||
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
||||||
const body = await readBody(input, init);
|
const body = await readBody(input, init);
|
||||||
|
|
||||||
if (path === "/ads") return json({ slots: [] });
|
if (path === "/promotions") return json({ slots: [] });
|
||||||
if (path === "/models" && method === "GET") return json(models);
|
if (path === "/models" && method === "GET") return json(models);
|
||||||
if (path === "/models" && method === "POST") return json(models);
|
if (path === "/models" && method === "POST") return json(models);
|
||||||
if (path === "/models/order") return json(models);
|
if (path === "/models/order") return json(models);
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ export function HomePage() {
|
|||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const [preset, setPreset] = useState<OverviewRangePreset>("month");
|
const [preset, setPreset] = useState<OverviewRangePreset>("month");
|
||||||
const [quick, setQuick] = useState<QuickPreset | null>(null);
|
const [quick, setQuick] = useState<QuickPreset | null>(null);
|
||||||
const [fourHourBucket, setFourHourBucket] = useState<number | undefined>(undefined);
|
|
||||||
const [customRange, setCustomRange] = useState<TimeRange | null>(null);
|
const [customRange, setCustomRange] = useState<TimeRange | null>(null);
|
||||||
const [customOpen, setCustomOpen] = useState(false);
|
const [customOpen, setCustomOpen] = useState(false);
|
||||||
const [customStart, setCustomStart] = useState("");
|
const [customStart, setCustomStart] = useState("");
|
||||||
@@ -76,14 +75,13 @@ export function HomePage() {
|
|||||||
void api.overview({
|
void api.overview({
|
||||||
...selectedRange,
|
...selectedRange,
|
||||||
modelHashes: appliedModels,
|
modelHashes: appliedModels,
|
||||||
bucketMs: fourHourBucket,
|
|
||||||
}).then((next) => {
|
}).then((next) => {
|
||||||
if (active) setRangeOverview(next);
|
if (active) setRangeOverview(next);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
if (active) setRangeBusy(false);
|
if (active) setRangeBusy(false);
|
||||||
});
|
});
|
||||||
return () => { active = false; };
|
return () => { active = false; };
|
||||||
}, [preset, customRange, overview, refreshVersion, appliedModels, fourHourBucket]);
|
}, [preset, customRange, overview, refreshVersion, appliedModels]);
|
||||||
|
|
||||||
const filteredOverview = rangeOverview ?? overview;
|
const filteredOverview = rangeOverview ?? overview;
|
||||||
const dailyTokenUsage = filteredOverview.token_usage_series.map((bucket) => ({
|
const dailyTokenUsage = filteredOverview.token_usage_series.map((bucket) => ({
|
||||||
@@ -118,11 +116,10 @@ export function HomePage() {
|
|||||||
setCustomRange({ startMs, endMs });
|
setCustomRange({ startMs, endMs });
|
||||||
setAppliedModels(selectedModels);
|
setAppliedModels(selectedModels);
|
||||||
setQuick(null);
|
setQuick(null);
|
||||||
setFourHourBucket(undefined);
|
|
||||||
setPreset("custom");
|
setPreset("custom");
|
||||||
setCustomOpen(false);
|
setCustomOpen(false);
|
||||||
};
|
};
|
||||||
const selectQuick = (durationMs: number, bucketMs?: number) => {
|
const selectQuick = (durationMs: number) => {
|
||||||
const end = new Date();
|
const end = new Date();
|
||||||
const start = new Date(end.getTime() - durationMs);
|
const start = new Date(end.getTime() - durationMs);
|
||||||
setCustomStart(formatTimeInput(start));
|
setCustomStart(formatTimeInput(start));
|
||||||
@@ -130,13 +127,12 @@ export function HomePage() {
|
|||||||
setCustomRange({ startMs: start.getTime(), endMs: end.getTime() });
|
setCustomRange({ startMs: start.getTime(), endMs: end.getTime() });
|
||||||
setAppliedModels(selectedModels);
|
setAppliedModels(selectedModels);
|
||||||
setQuick(durationMs === 4 * 60 * 60_000 ? "four-hours" : "twenty-four-hours");
|
setQuick(durationMs === 4 * 60 * 60_000 ? "four-hours" : "twenty-four-hours");
|
||||||
setFourHourBucket(durationMs === 4 * 60 * 60_000 ? bucketMs : undefined);
|
|
||||||
setPreset("custom");
|
setPreset("custom");
|
||||||
|
setCustomOpen(false);
|
||||||
};
|
};
|
||||||
const selectPreset = (value: Exclude<OverviewRangePreset, "custom">) => {
|
const selectPreset = (value: Exclude<OverviewRangePreset, "custom">) => {
|
||||||
setPreset(value);
|
setPreset(value);
|
||||||
setQuick(null);
|
setQuick(null);
|
||||||
setFourHourBucket(undefined);
|
|
||||||
setCustomOpen(false);
|
setCustomOpen(false);
|
||||||
};
|
};
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
@@ -152,7 +148,7 @@ export function HomePage() {
|
|||||||
icon: iconFor(model.type),
|
icon: iconFor(model.type),
|
||||||
})),
|
})),
|
||||||
...plugins.flatMap((plugin) => plugin.providers.flatMap((provider) =>
|
...plugins.flatMap((plugin) => plugin.providers.flatMap((provider) =>
|
||||||
provider.configured ? provider.models.map((model) => ({
|
provider.configured ? provider.models.filter((model) => model.enabled).map((model) => ({
|
||||||
value: model.id,
|
value: model.id,
|
||||||
label: model.displayName,
|
label: model.displayName,
|
||||||
group: pluginText(provider.displayName, locale) || model.pluginName,
|
group: pluginText(provider.displayName, locale) || model.pluginName,
|
||||||
@@ -187,7 +183,6 @@ export function HomePage() {
|
|||||||
<PageActions><OverviewTimeRangeFilter
|
<PageActions><OverviewTimeRangeFilter
|
||||||
value={preset}
|
value={preset}
|
||||||
quick={quick}
|
quick={quick}
|
||||||
fourHourBucket={fourHourBucket}
|
|
||||||
customOpen={customOpen}
|
customOpen={customOpen}
|
||||||
customStart={customStart}
|
customStart={customStart}
|
||||||
customEnd={customEnd}
|
customEnd={customEnd}
|
||||||
|
|||||||
@@ -95,31 +95,3 @@
|
|||||||
&[aria-pressed="true"] { color: var(--vscode-list-activeSelectionForeground); background: var(--vscode-list-activeSelectionBackground); border-color: transparent; }
|
&[aria-pressed="true"] { color: var(--vscode-list-activeSelectionForeground); background: var(--vscode-list-activeSelectionBackground); border-color: transparent; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.granularityMenu {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 15000;
|
|
||||||
min-width: 96px;
|
|
||||||
padding: 4px;
|
|
||||||
background: var(--vscode-dropdown-background);
|
|
||||||
border: 1px solid var(--vscode-dropdown-border);
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: var(--oa-dropdown-shadow);
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 28px;
|
|
||||||
display: block;
|
|
||||||
padding: 4px 8px;
|
|
||||||
color: var(--vscode-dropdown-foreground);
|
|
||||||
text-align: left;
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: type.$font-size-xs;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&:hover { background: var(--vscode-list-activeSelectionBackground); color: var(--vscode-list-activeSelectionForeground); }
|
|
||||||
&[aria-pressed="true"] { background: var(--vscode-list-activeSelectionBackground); color: var(--vscode-list-activeSelectionForeground); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import styles from "./OverviewTimeRangeFilter.module.scss";
|
|||||||
export type OverviewRangePreset = "ten-minutes" | "hour" | "today" | "week" | "month" | "custom";
|
export type OverviewRangePreset = "ten-minutes" | "hour" | "today" | "week" | "month" | "custom";
|
||||||
export type QuickPreset = "four-hours" | "twenty-four-hours";
|
export type QuickPreset = "four-hours" | "twenty-four-hours";
|
||||||
|
|
||||||
export function OverviewTimeRangeFilter({ value, quick, fourHourBucket, customOpen, customStart, customEnd, modelOptions, selectedModels, busy, onSelect, onQuickSelect, onCustomOpenChange, onCustomStartChange, onCustomEndChange, onSelectedModelsChange, onCustomApply, onRefresh }: {
|
export function OverviewTimeRangeFilter({ value, quick, customOpen, customStart, customEnd, modelOptions, selectedModels, busy, onSelect, onQuickSelect, onCustomOpenChange, onCustomStartChange, onCustomEndChange, onSelectedModelsChange, onCustomApply, onRefresh }: {
|
||||||
value: OverviewRangePreset;
|
value: OverviewRangePreset;
|
||||||
quick: QuickPreset | null;
|
quick: QuickPreset | null;
|
||||||
fourHourBucket: number | undefined;
|
|
||||||
customOpen: boolean;
|
customOpen: boolean;
|
||||||
customStart: string;
|
customStart: string;
|
||||||
customEnd: string;
|
customEnd: string;
|
||||||
@@ -23,7 +22,7 @@ export function OverviewTimeRangeFilter({ value, quick, fourHourBucket, customOp
|
|||||||
selectedModels: string[];
|
selectedModels: string[];
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
onSelect: (value: Exclude<OverviewRangePreset, "custom">) => void;
|
onSelect: (value: Exclude<OverviewRangePreset, "custom">) => void;
|
||||||
onQuickSelect: (durationMs: number, bucketMs?: number) => void;
|
onQuickSelect: (durationMs: number) => void;
|
||||||
onCustomOpenChange: (open: boolean) => void;
|
onCustomOpenChange: (open: boolean) => void;
|
||||||
onCustomStartChange: (value: string) => void;
|
onCustomStartChange: (value: string) => void;
|
||||||
onCustomEndChange: (value: string) => void;
|
onCustomEndChange: (value: string) => void;
|
||||||
@@ -42,21 +41,10 @@ export function OverviewTimeRangeFilter({ value, quick, fourHourBucket, customOp
|
|||||||
{ value: "four-hours", label: t("近4小时") },
|
{ value: "four-hours", label: t("近4小时") },
|
||||||
{ value: "twenty-four-hours", label: t("近24小时") },
|
{ value: "twenty-four-hours", label: t("近24小时") },
|
||||||
];
|
];
|
||||||
const granularityOptions = [
|
|
||||||
{ bucketMs: 60_000, label: t("1分钟") },
|
|
||||||
{ bucketMs: 15 * 60_000, label: t("15分钟") },
|
|
||||||
{ bucketMs: 30 * 60_000, label: t("30分钟") },
|
|
||||||
{ bucketMs: 60 * 60_000, label: t("1小时") },
|
|
||||||
];
|
|
||||||
const customButton = useRef<HTMLButtonElement>(null);
|
const customButton = useRef<HTMLButtonElement>(null);
|
||||||
const popover = useRef<HTMLDivElement>(null);
|
const popover = useRef<HTMLDivElement>(null);
|
||||||
const fourHourButton = useRef<HTMLButtonElement>(null);
|
|
||||||
const granularityMenu = useRef<HTMLDivElement>(null);
|
|
||||||
const popoverId = useId();
|
const popoverId = useId();
|
||||||
const granularityMenuId = useId();
|
|
||||||
const [position, setPosition] = useState({ left: 0, top: 0, width: 300, maxHeight: 480 });
|
const [position, setPosition] = useState({ left: 0, top: 0, width: 300, maxHeight: 480 });
|
||||||
const [menuPosition, setMenuPosition] = useState({ left: 0, top: 0 });
|
|
||||||
const [granularityOpen, setGranularityOpen] = useState(false);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!customOpen || !customButton.current || !popover.current) return;
|
if (!customOpen || !customButton.current || !popover.current) return;
|
||||||
@@ -72,28 +60,11 @@ export function OverviewTimeRangeFilter({ value, quick, fourHourBucket, customOp
|
|||||||
}).then(({ x, y }) => setPosition((current) => ({ ...current, left: x, top: y }))));
|
}).then(({ x, y }) => setPosition((current) => ({ ...current, left: x, top: y }))));
|
||||||
}, [customOpen]);
|
}, [customOpen]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!granularityOpen || !fourHourButton.current || !granularityMenu.current) return;
|
|
||||||
return autoUpdate(fourHourButton.current, granularityMenu.current, () =>
|
|
||||||
void computePosition(fourHourButton.current!, granularityMenu.current!, {
|
|
||||||
placement: "right-start",
|
|
||||||
middleware: [offset(4), flip({ padding: 10 }), shift({ padding: 10 })],
|
|
||||||
}).then(({ x, y }) => setMenuPosition({ left: x, top: y })));
|
|
||||||
}, [granularityOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!customOpen) setGranularityOpen(false);
|
|
||||||
}, [customOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!customOpen) return;
|
if (!customOpen) return;
|
||||||
const closeOutside = (event: PointerEvent) => {
|
const closeOutside = (event: PointerEvent) => {
|
||||||
const target = event.target as Node;
|
const target = event.target as Node;
|
||||||
if (
|
if (!customButton.current?.contains(target) && !popover.current?.contains(target)) onCustomOpenChange(false);
|
||||||
!customButton.current?.contains(target)
|
|
||||||
&& !popover.current?.contains(target)
|
|
||||||
&& !granularityMenu.current?.contains(target)
|
|
||||||
) onCustomOpenChange(false);
|
|
||||||
};
|
};
|
||||||
document.addEventListener("pointerdown", closeOutside);
|
document.addEventListener("pointerdown", closeOutside);
|
||||||
return () => document.removeEventListener("pointerdown", closeOutside);
|
return () => document.removeEventListener("pointerdown", closeOutside);
|
||||||
@@ -102,11 +73,6 @@ export function OverviewTimeRangeFilter({ value, quick, fourHourBucket, customOp
|
|||||||
const parsedStart = parseTimeInput(customStart);
|
const parsedStart = parseTimeInput(customStart);
|
||||||
const parsedEnd = parseTimeInput(customEnd);
|
const parsedEnd = parseTimeInput(customEnd);
|
||||||
const customValid = parsedStart !== null && parsedEnd !== null && parsedStart < parsedEnd;
|
const customValid = parsedStart !== null && parsedEnd !== null && parsedStart < parsedEnd;
|
||||||
const closeGranularity = () => {
|
|
||||||
setGranularityOpen(false);
|
|
||||||
fourHourButton.current?.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div className={styles.root} aria-label={t("概览时间范围")}>
|
return <div className={styles.root} aria-label={t("概览时间范围")}>
|
||||||
<div className={styles.presets}>
|
<div className={styles.presets}>
|
||||||
{presets.map((preset) => <button
|
{presets.map((preset) => <button
|
||||||
@@ -135,41 +101,21 @@ export function OverviewTimeRangeFilter({ value, quick, fourHourBucket, customOp
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-label={t("自定义概览筛选")}
|
aria-label={t("自定义概览筛选")}
|
||||||
style={position}
|
style={position}
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (granularityOpen) {
|
|
||||||
closeGranularity();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onCustomOpenChange(false);
|
onCustomOpenChange(false);
|
||||||
customButton.current?.focus();
|
customButton.current?.focus();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.quickPresets} aria-label={t("快捷时间范围")}>
|
<div className={styles.quickPresets} aria-label={t("快捷时间范围")}>
|
||||||
{quickPresets.map((preset) => preset.value === "four-hours"
|
{quickPresets.map((preset) => <button
|
||||||
? <button
|
|
||||||
key={preset.value}
|
|
||||||
ref={fourHourButton}
|
|
||||||
type="button"
|
|
||||||
aria-haspopup="menu"
|
|
||||||
aria-controls={granularityOpen ? granularityMenuId : undefined}
|
|
||||||
aria-expanded={granularityOpen}
|
|
||||||
aria-pressed={quick === "four-hours"}
|
|
||||||
onClick={() => {
|
|
||||||
onQuickSelect(4 * 60 * 60_000, fourHourBucket);
|
|
||||||
setGranularityOpen((current) => !current);
|
|
||||||
}}
|
|
||||||
>{preset.label}</button>
|
|
||||||
: <button
|
|
||||||
key={preset.value}
|
key={preset.value}
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={quick === "twenty-four-hours"}
|
aria-pressed={quick === preset.value}
|
||||||
onClick={() => {
|
onClick={() => onQuickSelect(preset.value === "four-hours" ? 4 * 60 * 60_000 : 24 * 60 * 60_000)}
|
||||||
setGranularityOpen(false);
|
|
||||||
onQuickSelect(24 * 60 * 60_000);
|
|
||||||
}}
|
|
||||||
>{preset.label}</button>)}
|
>{preset.label}</button>)}
|
||||||
</div>
|
</div>
|
||||||
<label><span>{t("开始时间")}</span><input type="text" placeholder={t("如:2026-08-23 09:00、1小时前")} value={customStart} onChange={(event) => onCustomStartChange(event.target.value)} /></label>
|
<label><span>{t("开始时间")}</span><input type="text" placeholder={t("如:2026-08-23 09:00、1小时前")} value={customStart} onChange={(event) => onCustomStartChange(event.target.value)} /></label>
|
||||||
@@ -180,31 +126,5 @@ export function OverviewTimeRangeFilter({ value, quick, fourHourBucket, customOp
|
|||||||
<button type="button" className={controls.primary} disabled={!customValid} onClick={onCustomApply}>{t("应用")}</button>
|
<button type="button" className={controls.primary} disabled={!customValid} onClick={onCustomApply}>{t("应用")}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>, document.body)}
|
</div>, document.body)}
|
||||||
{granularityOpen && customOpen && createPortal(<div
|
|
||||||
id={granularityMenuId}
|
|
||||||
ref={granularityMenu}
|
|
||||||
className={styles.granularityMenu}
|
|
||||||
role="menu"
|
|
||||||
aria-label={t("显示粒度")}
|
|
||||||
style={menuPosition}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
closeGranularity();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{granularityOptions.map((option) => <button
|
|
||||||
key={option.bucketMs}
|
|
||||||
type="button"
|
|
||||||
role="menuitem"
|
|
||||||
aria-pressed={quick === "four-hours" && fourHourBucket === option.bucketMs}
|
|
||||||
onClick={() => {
|
|
||||||
setGranularityOpen(false);
|
|
||||||
onQuickSelect(4 * 60 * 60_000, option.bucketMs);
|
|
||||||
}}
|
|
||||||
>{option.label}</button>)}
|
|
||||||
</div>, document.body)}
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export function PluginManagementPage() {
|
|||||||
? t("{name} 账号管理", { name: selectedPlugin?.name ?? "" })
|
? t("{name} 账号管理", { name: selectedPlugin?.name ?? "" })
|
||||||
: t("添加 {name} 账号", { name: selectedPlugin?.name ?? "" })}
|
: t("添加 {name} 账号", { name: selectedPlugin?.name ?? "" })}
|
||||||
onClose={() => setSelected(null)}
|
onClose={() => setSelected(null)}
|
||||||
|
onSubmit={() => setSelected(null)}
|
||||||
|
submitLabel={t("确定")}
|
||||||
>
|
>
|
||||||
{selected?.mode === "add" && selectedPlugin && <PluginAddPanel plugin={selectedPlugin} onConfigured={() => setSelected(null)} />}
|
{selected?.mode === "add" && selectedPlugin && <PluginAddPanel plugin={selectedPlugin} onConfigured={() => setSelected(null)} />}
|
||||||
{selected?.mode === "settings" && selectedPlugin && <PluginSettingsPanel plugin={selectedPlugin} />}
|
{selected?.mode === "settings" && selectedPlugin && <PluginSettingsPanel plugin={selectedPlugin} />}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export function CommitSettingsCard() {
|
|||||||
for (const provider of plugin.providers) {
|
for (const provider of plugin.providers) {
|
||||||
if (!provider.configured) continue;
|
if (!provider.configured) continue;
|
||||||
const group = pluginText(provider.displayName, locale) || plugin.name;
|
const group = pluginText(provider.displayName, locale) || plugin.name;
|
||||||
for (const model of provider.models) {
|
for (const model of provider.models.filter((model) => model.enabled)) {
|
||||||
seen.add(model.id);
|
seen.add(model.id);
|
||||||
options.push({
|
options.push({
|
||||||
value: model.id,
|
value: model.id,
|
||||||
|
|||||||
@@ -409,7 +409,7 @@
|
|||||||
"f9aa11dbb15ce647": "Saturday",
|
"f9aa11dbb15ce647": "Saturday",
|
||||||
"f9b55ca75425161b": "Response content was not recorded. Enable detailed records and try again.",
|
"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.",
|
"fa5b4b8a751c7d1b": "The local proxy port used by Cursor. Enter 0 to select a random port at startup.",
|
||||||
"fac2a67ad87807c4": "",
|
"fac2a67ad87807c4": "Confirm",
|
||||||
"fad86bf65f72c747": "Download progress",
|
"fad86bf65f72c747": "Download progress",
|
||||||
"fb11aa6f29827095": "Checking…",
|
"fb11aa6f29827095": "Checking…",
|
||||||
"fbe8778fa8b9bab5": "Initialize the local CA first",
|
"fbe8778fa8b9bab5": "Initialize the local CA first",
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ export interface PluginImportResult {
|
|||||||
|
|
||||||
export function configuredPluginModels(plugins: PluginDescriptor[]): PluginModelDescriptor[] {
|
export function configuredPluginModels(plugins: PluginDescriptor[]): PluginModelDescriptor[] {
|
||||||
return plugins.flatMap((plugin) =>
|
return plugins.flatMap((plugin) =>
|
||||||
plugin.providers.flatMap((provider) => provider.configured ? provider.models : []));
|
plugin.providers.flatMap((provider) => provider.configured ? provider.models.filter((model) => model.enabled) : []));
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OverviewMetrics {
|
export interface OverviewMetrics {
|
||||||
@@ -437,14 +437,14 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
export const api = {
|
export const api = {
|
||||||
ads: (disabledAdIds: Iterable<string>, locale: Locale) => {
|
ads: (disabledAdIds: Iterable<string>, locale: Locale) => {
|
||||||
const value = [...disabledAdIds].join(",");
|
const value = [...disabledAdIds].join(",");
|
||||||
return request<AdRuntime>("/ads", {
|
return request<AdRuntime>("/promotions", {
|
||||||
headers: {
|
headers: {
|
||||||
"accept-language": locale,
|
"accept-language": locale,
|
||||||
...(value ? { "disable-ad-ids": value } : {}),
|
...(value ? { "disable-ad-ids": value } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
dismissAd: (id: string, reason: string) => request<void>(`/ads/${encodeURIComponent(id)}/dismissals`, { method: "POST", body: JSON.stringify({ reason }) }),
|
dismissAd: (id: string, reason: string) => request<void>(`/promotions/${encodeURIComponent(id)}/dismissals`, { method: "POST", body: JSON.stringify({ reason }) }),
|
||||||
models: () => request<Model[]>("/models"),
|
models: () => request<Model[]>("/models"),
|
||||||
createModels: (models: ModelInput[]) => request<Model[]>("/models", { method: "POST", body: JSON.stringify({ models }) }),
|
createModels: (models: ModelInput[]) => request<Model[]>("/models", { method: "POST", body: JSON.stringify({ models }) }),
|
||||||
reorderModels: (modelHashes: string[]) => request<Model[]>("/models/order", { method: "PUT", body: JSON.stringify({ model_hashes: modelHashes }) }),
|
reorderModels: (modelHashes: string[]) => request<Model[]>("/models/order", { method: "PUT", body: JSON.stringify({ model_hashes: modelHashes }) }),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RefObject } from "react";
|
import { useLayoutEffect, useRef, useState, type RefObject } from "react";
|
||||||
|
import { Marquee } from "react-css-marquee";
|
||||||
import { Icon } from "../../shared/ui/Icon";
|
import { Icon } from "../../shared/ui/Icon";
|
||||||
import { windowCloseIcon } from "../../shared/ui/icons";
|
import { windowCloseIcon } from "../../shared/ui/icons";
|
||||||
import type { AdSlot } from "./types";
|
import type { AdSlot } from "./types";
|
||||||
@@ -14,6 +15,32 @@ type AdMenuProps = {
|
|||||||
onDismiss: (ad: AdSlot) => void;
|
onDismiss: (ad: AdSlot) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type OverflowTextProps = {
|
||||||
|
children: string;
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function OverflowText({ children, className }: OverflowTextProps) {
|
||||||
|
const text = useRef<HTMLSpanElement>(null);
|
||||||
|
const [overflowing, setOverflowing] = useState(false);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const element = text.current;
|
||||||
|
if (!element) return;
|
||||||
|
const update = () => setOverflowing(element.scrollWidth > element.clientWidth + 1);
|
||||||
|
update();
|
||||||
|
const observer = new ResizeObserver(update);
|
||||||
|
observer.observe(element);
|
||||||
|
if (element.parentElement) observer.observe(element.parentElement);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
if (!overflowing) return <span ref={text} className={className}>{children}</span>;
|
||||||
|
return <Marquee className={styles.menuMarquee} repeat loop={0} speed={4} gap={24}>
|
||||||
|
<span ref={text} className={className}>{children}</span>
|
||||||
|
</Marquee>;
|
||||||
|
}
|
||||||
|
|
||||||
export function AdMenu({ ads, activeAdId, dismissingAdId, readAdIds, triggerRefs, onOpen, onDismiss }: AdMenuProps) {
|
export function AdMenu({ ads, activeAdId, dismissingAdId, readAdIds, triggerRefs, onOpen, onDismiss }: AdMenuProps) {
|
||||||
return <div className={styles.menuSlots} aria-label={t("推荐内容")}>
|
return <div className={styles.menuSlots} aria-label={t("推荐内容")}>
|
||||||
{ads.map((ad) => <div key={ad.id} className={styles.menuSlotContainer}>
|
{ads.map((ad) => <div key={ad.id} className={styles.menuSlotContainer}>
|
||||||
@@ -34,8 +61,8 @@ export function AdMenu({ ads, activeAdId, dismissingAdId, readAdIds, triggerRefs
|
|||||||
{!readAdIds.has(ad.id) && <span className={styles.unreadDot} aria-hidden="true" />}
|
{!readAdIds.has(ad.id) && <span className={styles.unreadDot} aria-hidden="true" />}
|
||||||
<img className={styles.menuImage} src={ad.target.imageUrl} alt="" />
|
<img className={styles.menuImage} src={ad.target.imageUrl} alt="" />
|
||||||
<span className={styles.menuCopy}>
|
<span className={styles.menuCopy}>
|
||||||
<span className={styles.menuTitle}>{ad.target.title}</span>
|
<OverflowText className={styles.menuTitle}>{ad.target.title}</OverflowText>
|
||||||
<span className={styles.menuSubtitle}>{ad.target.description}</span>
|
<OverflowText className={styles.menuSubtitle}>{ad.target.description}</OverflowText>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -25,18 +25,13 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background: color-mix(in srgb, var(--vscode-foreground) 4%, transparent);
|
background: color-mix(in srgb, var(--vscode-foreground) 4%, transparent);
|
||||||
border: 1px solid var(--vscode-sideBar-border);
|
border: 2px solid #be8d180d;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&[aria-expanded="true"] {
|
&[aria-expanded="true"] {
|
||||||
background: color-mix(in srgb, var(--vscode-foreground) 9%, transparent);
|
background: color-mix(in srgb, var(--vscode-foreground) 9%, transparent);
|
||||||
border-color: var(--vscode-sideBar-border);
|
border-color: #be8d180d;
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 1px solid var(--vscode-focusBorder);
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +42,7 @@
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: color-mix(in srgb, var(--vscode-descriptionForeground) 42%, transparent);
|
color: color-mix(in srgb, var(--vscode-descriptionForeground) 42%, transparent);
|
||||||
@@ -66,6 +61,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menuSlotContainer:hover .menuDismiss {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.unreadDot {
|
.unreadDot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
@@ -93,11 +92,24 @@
|
|||||||
|
|
||||||
.menuTitle,
|
.menuTitle,
|
||||||
.menuSubtitle {
|
.menuSubtitle {
|
||||||
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menuMarquee {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuMarquee .menuTitle,
|
||||||
|
.menuMarquee .menuSubtitle {
|
||||||
|
width: max-content;
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
.menuTitle {
|
.menuTitle {
|
||||||
font-size: type.$font-size-xs;
|
font-size: type.$font-size-xs;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -121,9 +133,9 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
background: var(--vscode-editorWidget-background);
|
background: color-mix(in srgb, var(--vscode-editorWidget-background) 92%, var(--vscode-foreground) 8%);
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
border: none;
|
border: 1px solid color-mix(in srgb, var(--vscode-foreground) 12%, transparent);
|
||||||
box-shadow: 0 24px 20px rgb(0 0 0 / 28%);
|
box-shadow: 0 24px 20px rgb(0 0 0 / 28%);
|
||||||
animation: ad-enter 180ms ease-out;
|
animation: ad-enter 180ms ease-out;
|
||||||
|
|
||||||
@@ -132,6 +144,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(:root[data-theme="default-light"]) .floatingAd {
|
||||||
|
background: var(--vscode-editorWidget-background);
|
||||||
|
}
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
flex: 0 0 112px;
|
flex: 0 0 112px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"apiVersion": 1,
|
"apiVersion": 1,
|
||||||
"id": "dev.cursorbyok.plugins.antigravity-auth",
|
"id": "dev.cursorbyok.plugins.antigravity-auth",
|
||||||
"name": "Antigravity",
|
"name": "Antigravity",
|
||||||
"version": "0.3.1",
|
"version": "0.1.7",
|
||||||
"author": "@masudranaxpert",
|
"author": "@masudranaxpert",
|
||||||
"minAppVersion": "0.1.0",
|
"minAppVersion": "0.1.0",
|
||||||
"icon": "assets/antigravity.svg",
|
"icon": "assets/antigravity.svg",
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { PluginContext } from "cursor-byok:plugin";
|
||||||
|
import { parseCredentialFiles } from "./resources.ts";
|
||||||
|
|
||||||
|
function assert(condition: unknown, message: string): asserts condition {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function context(requests: string[]): PluginContext["network"] {
|
||||||
|
return {
|
||||||
|
fetch: async (url, init = {}) => {
|
||||||
|
requests.push(`${url}:${init.body ?? ""}`);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
headers: {},
|
||||||
|
body: JSON.stringify({
|
||||||
|
access_token: "access-token",
|
||||||
|
refresh_token: "refresh-token",
|
||||||
|
expires_in: 3600,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
stream: () => Promise.reject(new Error("stream is not expected")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test("imports an array of email and refresh_token credentials", async () => {
|
||||||
|
const requests: string[] = [];
|
||||||
|
const result = await parseCredentialFiles([
|
||||||
|
{
|
||||||
|
name: "antigravity.json",
|
||||||
|
content: JSON.stringify([
|
||||||
|
{ email: "teocougar@gmail.com", refresh_token: "refresh-token" },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
], context(requests));
|
||||||
|
|
||||||
|
assert(result.warnings.length === 0, "the credential file should not produce warnings");
|
||||||
|
assert(result.credentials.length === 1, "one credential should be imported");
|
||||||
|
assert(result.credentials[0].displayName === "teocougar@gmail.com", "email should be used as display name");
|
||||||
|
assert(result.credentials[0].accessToken === "access-token", "refresh token should be exchanged for an access token");
|
||||||
|
assert(result.credentials[0].refreshToken === "refresh-token", "refresh token should be preserved");
|
||||||
|
assert(requests.length === 1, "the refresh token should be exchanged once");
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"apiVersion": 1,
|
"apiVersion": 1,
|
||||||
"id": "dev.cursorbyok.examples.codex-auth",
|
"id": "dev.cursorbyok.examples.codex-auth",
|
||||||
"name": "Codex",
|
"name": "Codex",
|
||||||
"version": "0.1.0",
|
"version": "0.1.7",
|
||||||
"author": "@leookun",
|
"author": "@leookun",
|
||||||
"minAppVersion": "0.1.0",
|
"minAppVersion": "0.1.0",
|
||||||
"icon": "assets/codex.svg",
|
"icon": "assets/codex.svg",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"apiVersion": 1,
|
"apiVersion": 1,
|
||||||
"id": "dev.cursorbyok.examples.grok-auth",
|
"id": "dev.cursorbyok.examples.grok-auth",
|
||||||
"name": "Grok",
|
"name": "Grok",
|
||||||
"version": "0.1.0",
|
"version": "0.1.7",
|
||||||
"author": "@leookun",
|
"author": "@leookun",
|
||||||
"minAppVersion": "0.1.0",
|
"minAppVersion": "0.1.0",
|
||||||
"icon": "assets/grok.svg",
|
"icon": "assets/grok.svg",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ pub(super) const OS_HEADER: &str = "X-Cursor-Assistant-OS";
|
|||||||
pub(super) const APP_VERSION_HEADER: &str = "X-Cursor-Assistant-Version";
|
pub(super) const APP_VERSION_HEADER: &str = "X-Cursor-Assistant-Version";
|
||||||
pub(super) const DISABLED_AD_IDS_HEADER: &str = "disable-ad-ids";
|
pub(super) const DISABLED_AD_IDS_HEADER: &str = "disable-ad-ids";
|
||||||
pub(super) const LANGUAGE_HEADER: &str = "accept-language";
|
pub(super) const LANGUAGE_HEADER: &str = "accept-language";
|
||||||
const ADS_IMAGE_ROUTE: &str = "/__byok-api__/api/ads/images";
|
const ADS_IMAGE_ROUTE: &str = "/__byok-api__/api/promotions/images";
|
||||||
const MAX_AD_IMAGE_BYTES: usize = 10 * 1024 * 1024;
|
const MAX_AD_IMAGE_BYTES: usize = 10 * 1024 * 1024;
|
||||||
const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "gif", "webp", "avif"];
|
const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "gif", "webp", "avif"];
|
||||||
|
|
||||||
|
|||||||
@@ -111,10 +111,13 @@ fn proxy_error(error: impl std::fmt::Display) -> Response<Body> {
|
|||||||
|
|
||||||
pub fn api_router(service: ControlService) -> Router {
|
pub fn api_router(service: ControlService) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/__byok-api__/api/ads", get(ads::get))
|
.route("/__byok-api__/api/promotions", get(ads::get))
|
||||||
.route("/__byok-api__/api/ads/images/{file_name}", get(ads::image))
|
|
||||||
.route(
|
.route(
|
||||||
"/__byok-api__/api/ads/{ad_id}/dismissals",
|
"/__byok-api__/api/promotions/images/{file_name}",
|
||||||
|
get(ads::image),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/__byok-api__/api/promotions/{ad_id}/dismissals",
|
||||||
post(ads::dismiss),
|
post(ads::dismiss),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use serde_json::Value;
|
|||||||
|
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
|
|
||||||
|
const NO_PROXY_KEY: &str = "http.noProxy";
|
||||||
const KEYS: [&str; 5] = [
|
const KEYS: [&str; 5] = [
|
||||||
"http.proxy",
|
"http.proxy",
|
||||||
"http.proxyKerberosServicePrincipal",
|
"http.proxyKerberosServicePrincipal",
|
||||||
@@ -60,6 +61,7 @@ fn write(settings: &BTreeMap<String, Value>) -> Result<()> {
|
|||||||
|
|
||||||
pub fn write_proxy_settings(proxy_url: &str) -> Result<()> {
|
pub fn write_proxy_settings(proxy_url: &str) -> Result<()> {
|
||||||
let mut settings = read()?;
|
let mut settings = read()?;
|
||||||
|
settings.remove(NO_PROXY_KEY);
|
||||||
settings.insert(KEYS[0].into(), Value::String(proxy_url.into()));
|
settings.insert(KEYS[0].into(), Value::String(proxy_url.into()));
|
||||||
settings.insert(KEYS[1].into(), Value::String(proxy_url.into()));
|
settings.insert(KEYS[1].into(), Value::String(proxy_url.into()));
|
||||||
settings.insert(KEYS[2].into(), Value::String("on".into()));
|
settings.insert(KEYS[2].into(), Value::String("on".into()));
|
||||||
|
|||||||
Reference in New Issue
Block a user