1import type { EnvironmentId, UnifiedSettings } from "@t3tools/contracts";
2import { useAtomValue } from "@effect/atom-react";
3import { PlusIcon } from "lucide-react";
4import { useState } from "react";
5
6import { useUpdateEnvironmentSettings } from "../../hooks/useSettings";
7import { serverEnvironment } from "../../state/server";
8import { useAtomCommand } from "../../state/use-atom-command";
9import {
10 AlertDialog,
11 AlertDialogClose,
12 AlertDialogDescription,
13 AlertDialogFooter,
14 AlertDialogHeader,
15 AlertDialogPopup,
16 AlertDialogTitle,
17} from "../ui/alert-dialog";
18import { Button } from "../ui/button";
19import { Switch } from "../ui/switch";
20import { AddUsageLimitSourceDialog } from "./AddUsageLimitSourceDialog";
21import { searchableSetting } from "./settingsSearch";
22import { SettingsRow, SettingsSection } from "./settingsLayout";
23
24/** Hub management follows the selected device and access rules of provider settings. */
25export function UsageProviderSettings({
26 environmentId,
27 environmentLabel,
28 sources,
29 cursorKeychainUsageEnabled,
30 readOnly,
31}: {
32 readonly environmentId: EnvironmentId;
33 readonly environmentLabel: string;
34 readonly sources: UnifiedSettings["usageLimitSources"];
35 readonly cursorKeychainUsageEnabled: boolean;
36 readonly readOnly: boolean;
37}) {
38 const updateSettings = useUpdateEnvironmentSettings(environmentId);
39 const updateCursorSettings = useAtomCommand(serverEnvironment.updateSettings, {
40 label: "update Cursor account usage",
41 });
42 const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, {
43 reportFailure: false,
44 });
45 const platform = useAtomValue(serverEnvironment.configValueAtom(environmentId))?.environment
46 .platform;
47 const [adding, setAdding] = useState(false);
48 const [updatingCursor, setUpdatingCursor] = useState(false);
49 const entries = Object.entries(sources);
50
51 const setCursorUsageEnabled = async (enabled: boolean) => {
52 setUpdatingCursor(true);
53 try {
54 const result = await updateCursorSettings({
55 environmentId,
56 input: { patch: { cursorKeychainUsageEnabled: enabled } },
57 });
58 if (result._tag === "Success") {
59 await refreshProviders({ environmentId, input: {} });
60 }
61 } finally {
62 setUpdatingCursor(false);
63 }
64 };
65
66 return (
67 <>
68 <SettingsSection
69 {...searchableSetting("usage-providers")}
70 headerAction={
71 !readOnly ? (
72 <Button size="xs" variant="outline" onClick={() => setAdding(true)}>
73 <PlusIcon className="size-3" aria-hidden />
74 Add hub
75 </Button>
76 ) : null
77 }
78 >
79 {platform?.os === "darwin" ? (
80 <SettingsRow
81 id="cursor-keychain-usage"
82 title="Cursor account usage"
83 description="Read your existing Cursor CLI login from macOS Keychain to show account history and monthly limits. macOS may ask you to allow access."
84 control={
85 <Switch
86 aria-label="Cursor account usage"
87 checked={cursorKeychainUsageEnabled}
88 disabled={readOnly || updatingCursor}
89 onCheckedChange={(enabled) => void setCursorUsageEnabled(enabled)}
90 />
91 }
92 />
93 ) : null}
94 {entries.length === 0 ? (
95 <SettingsRow title="No hubs configured." />
96 ) : (
97 entries.map(([id, source]) => {
98 const label = source.label?.trim() || source.url;
99 return (
100 <SettingsRow
101 key={id}
102 title={label}
103 description={
104 <span className="break-all">
105 CLI Proxy{source.enabled ? "" : " · Disabled"}
106 {label !== source.url ? ` · ${source.url}` : ""}
107 </span>
108 }
109 control={
110 !readOnly ? (
111 <RemoveUsageProviderButton
112 label={label}
113 onConfirm={() => updateSettings({ usageLimitSources: { [id]: null } })}
114 />
115 ) : null
116 }
117 />
118 );
119 })
120 )}
121 </SettingsSection>
122 {adding && !readOnly ? (
123 <AddUsageLimitSourceDialog
124 open
125 onOpenChange={setAdding}
126 environmentId={environmentId}
127 environmentLabel={environmentLabel}
128 />
129 ) : null}
130 </>
131 );
132}
133
134/** Removing a hub deletes its stored management key, so it requires confirmation. */
135function RemoveUsageProviderButton({
136 label,
137 onConfirm,
138}: {
139 readonly label: string;
140 readonly onConfirm: () => void;
141}) {
142 const [open, setOpen] = useState(false);
143 return (
144 <>
145 <Button size="xs" variant="ghost" onClick={() => setOpen(true)}>
146 Remove
147 </Button>
148 <AlertDialog open={open} onOpenChange={setOpen}>
149 <AlertDialogPopup>
150 <AlertDialogHeader>
151 <AlertDialogTitle>Remove {label}?</AlertDialogTitle>
152 <AlertDialogDescription>
153 The hub's management key is deleted from this server. Its accounts leave the Limits
154 view; the hub itself is untouched. Add it again with the URL and key to bring them
155 back.
156 </AlertDialogDescription>
157 </AlertDialogHeader>
158 <AlertDialogFooter>
159 <AlertDialogClose render={<Button variant="outline" />}>Cancel</AlertDialogClose>
160 <Button
161 variant="destructive"
162 onClick={() => {
163 setOpen(false);
164 onConfirm();
165 }}
166 >
167 Remove hub
168 </Button>
169 </AlertDialogFooter>
170 </AlertDialogPopup>
171 </AlertDialog>
172 </>
173 );
174}