fix(ui): move agy power-user mode to proxy settings

- move Antigravity power-user toggle from Settings > Auth to Settings > Proxy

- keep Add Account messaging consistent with the new location

- retain issue #509 risk framing for AGY flows
This commit is contained in:
Tam Nhu Tran
2026-02-24 18:23:14 +07:00
parent 6e20cdcdff
commit 30dccb14fe
3 changed files with 113 additions and 119 deletions
@@ -340,7 +340,7 @@ export function AddAccountDialog({
<ShieldAlert className="h-3.5 w-3.5" />
Power user mode enabled
</div>
AGY responsibility checklist is skipped from Settings {'>'} Auth. You accept full
AGY responsibility checklist is skipped from Settings {'>'} Proxy. You accept full
responsibility for OAuth/account risk.
</div>
)}
+10 -114
View File
@@ -3,12 +3,11 @@
* Settings section for CLIProxy auth tokens (API key and management secret)
*/
import { useEffect, useState, useCallback, useRef } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Switch } from '@/components/ui/switch';
import {
RefreshCw,
CheckCircle2,
@@ -21,7 +20,6 @@ import {
Check,
KeyRound,
ShieldCheck,
ShieldAlert,
Save,
} from 'lucide-react';
import { useRawConfig } from '../hooks';
@@ -53,10 +51,6 @@ export default function AuthSection() {
const [editedSecret, setEditedSecret] = useState<string | null>(null);
const [copiedApiKey, setCopiedApiKey] = useState(false);
const [copiedSecret, setCopiedSecret] = useState(false);
const [agyAckBypass, setAgyAckBypass] = useState(false);
const [agyAckBypassLoading, setAgyAckBypassLoading] = useState(true);
const [agyAckBypassSaving, setAgyAckBypassSaving] = useState(false);
const agyAckBypassSavingRef = useRef(false);
// Fetch tokens
const fetchTokens = useCallback(async () => {
@@ -77,28 +71,11 @@ export default function AuthSection() {
}
}, []);
const fetchAgyAckBypass = useCallback(async () => {
try {
setAgyAckBypassLoading(true);
const response = await fetch('/api/settings/auth/antigravity-risk');
if (!response.ok) {
throw new Error('Failed to load Antigravity power user settings');
}
const data = (await response.json()) as { antigravityAckBypass?: boolean };
setAgyAckBypass(data.antigravityAckBypass === true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setAgyAckBypassLoading(false);
}
}, []);
// Load on mount
useEffect(() => {
fetchTokens();
fetchAgyAckBypass();
fetchRawConfig();
}, [fetchTokens, fetchAgyAckBypass, fetchRawConfig]);
}, [fetchTokens, fetchRawConfig]);
// Clear success after timeout
useEffect(() => {
@@ -118,8 +95,6 @@ export default function AuthSection() {
// Save all changes
const saveChanges = async () => {
if (agyAckBypassSaving) return;
const hasApiKeyChange = editedApiKey !== null && editedApiKey !== tokens?.apiKey.value;
const hasSecretChange =
editedSecret !== null && editedSecret !== tokens?.managementSecret.value;
@@ -159,8 +134,6 @@ export default function AuthSection() {
// Regenerate management secret
const regenerateSecret = async () => {
if (agyAckBypassSaving) return;
try {
setSaving(true);
setError(null);
@@ -185,8 +158,6 @@ export default function AuthSection() {
// Reset to defaults
const resetToDefaults = async () => {
if (agyAckBypassSaving) return;
try {
setSaving(true);
setError(null);
@@ -226,50 +197,11 @@ export default function AuthSection() {
setTimeout(() => setCopiedSecret(false), 2000);
};
const saveAgyAckBypass = async (nextValue: boolean) => {
if (agyAckBypassSavingRef.current || agyAckBypassSaving || saving) return;
if (nextValue) {
const confirmed = window.confirm(
'Enable AGY power user mode?\n\nThis skips AGY safety prompts. You accept the OAuth risk.'
);
if (!confirmed) return;
}
try {
agyAckBypassSavingRef.current = true;
setAgyAckBypassSaving(true);
setError(null);
const response = await fetch('/api/settings/auth/antigravity-risk', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ antigravityAckBypass: nextValue }),
});
if (!response.ok) {
const data = (await response.json()) as { error?: string };
throw new Error(data.error || 'Failed to update Antigravity power user mode');
}
setAgyAckBypass(nextValue);
setSuccess(
nextValue ? 'Antigravity power user mode enabled.' : 'Antigravity power user mode disabled.'
);
await fetchRawConfig();
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
agyAckBypassSavingRef.current = false;
setAgyAckBypassSaving(false);
}
};
const refreshAll = async () => {
if (loading || saving || agyAckBypassSaving) return;
if (loading || saving) return;
setError(null);
setSuccess(null);
await Promise.all([fetchTokens(), fetchAgyAckBypass(), fetchRawConfig()]);
await Promise.all([fetchTokens(), fetchRawConfig()]);
};
if (loading || !tokens) {
@@ -344,7 +276,7 @@ export default function AuthSection() {
value={displayApiKey}
onChange={(e) => setEditedApiKey(e.target.value)}
placeholder="API key"
disabled={saving || agyAckBypassSaving}
disabled={saving}
className="pr-20 font-mono text-sm"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1">
@@ -395,7 +327,7 @@ export default function AuthSection() {
value={displaySecret}
onChange={(e) => setEditedSecret(e.target.value)}
placeholder="Management secret"
disabled={saving || agyAckBypassSaving}
disabled={saving}
className="pr-20 font-mono text-sm"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1">
@@ -426,7 +358,7 @@ export default function AuthSection() {
variant="outline"
size="sm"
onClick={regenerateSecret}
disabled={saving || agyAckBypassSaving}
disabled={saving}
title="Generate new secure secret"
>
<Sparkles className="w-4 h-4" />
@@ -434,48 +366,12 @@ export default function AuthSection() {
</div>
</div>
{/* Actions */}
<div className="space-y-3 rounded-lg border border-amber-400/35 bg-amber-50/70 p-4 dark:border-amber-800/60 dark:bg-amber-950/25">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-amber-700 dark:text-amber-300" />
<h3 className="text-base font-medium">Antigravity Power User Mode</h3>
</div>
<p className="text-xs text-muted-foreground">
Skip AGY responsibility checklists in Add Account and `ccs agy` flows.
</p>
</div>
<Switch
aria-labelledby="agy-power-user-mode-label"
aria-describedby="agy-power-user-mode-description"
checked={agyAckBypass}
disabled={agyAckBypassLoading || agyAckBypassSaving || saving}
onCheckedChange={saveAgyAckBypass}
/>
</div>
<p
id="agy-power-user-mode-description"
className="text-xs text-amber-800/90 dark:text-amber-200/90"
>
Use only if you fully understand the OAuth suspension/ban risk pattern (#509). CCS
cannot assume responsibility for account loss.
</p>
<span id="agy-power-user-mode-label" className="sr-only">
Toggle AGY power user mode
</span>
</div>
<div className="pt-4 border-t">
<Button
variant="outline"
size="sm"
onClick={resetToDefaults}
disabled={
saving ||
agyAckBypassSaving ||
(!tokens.apiKey.isCustom && !tokens.managementSecret.isCustom)
}
disabled={saving || (!tokens.apiKey.isCustom && !tokens.managementSecret.isCustom)}
className="gap-2"
>
<RotateCcw className="w-4 h-4" />
@@ -494,7 +390,7 @@ export default function AuthSection() {
variant="outline"
size="sm"
onClick={refreshAll}
disabled={loading || saving || agyAckBypassSaving}
disabled={loading || saving}
className="flex-1"
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
@@ -504,7 +400,7 @@ export default function AuthSection() {
variant="default"
size="sm"
onClick={saveChanges}
disabled={!hasChanges || saving || agyAckBypassSaving}
disabled={!hasChanges || saving}
className="flex-1"
>
<Save className={`w-4 h-4 mr-2 ${saving ? 'animate-pulse' : ''}`} />
+102 -4
View File
@@ -3,7 +3,7 @@
* Settings section for CLIProxyAPI configuration (local/remote)
*/
import { useEffect, useState, useCallback } from 'react';
import { useEffect, useState, useCallback, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { ScrollArea } from '@/components/ui/scroll-area';
@@ -17,6 +17,7 @@ import {
Bug,
Box,
AlertTriangle,
ShieldAlert,
} from 'lucide-react';
import { useProxyConfig, useRawConfig } from '../../hooks';
import { useUpdateBackend, useProxyStatus } from '@/hooks/use-cliproxy';
@@ -67,6 +68,10 @@ export default function ProxySection() {
return false;
}
});
const [agyAckBypass, setAgyAckBypass] = useState(false);
const [agyAckBypassLoading, setAgyAckBypassLoading] = useState(true);
const [agyAckBypassSaving, setAgyAckBypassSaving] = useState(false);
const agyAckBypassSavingRef = useRef(false);
const handleDebugModeChange = (enabled: boolean) => {
setDebugMode(enabled);
@@ -77,6 +82,62 @@ export default function ProxySection() {
}
};
const fetchAgyAckBypass = useCallback(async () => {
try {
setAgyAckBypassLoading(true);
const response = await fetch('/api/settings/auth/antigravity-risk');
if (!response.ok) {
throw new Error('Failed to load AGY power user mode');
}
const data = (await response.json()) as { antigravityAckBypass?: boolean };
setAgyAckBypass(data.antigravityAckBypass === true);
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to load AGY power user mode');
setAgyAckBypass(false);
} finally {
setAgyAckBypassLoading(false);
}
}, []);
const saveAgyAckBypass = useCallback(
async (nextValue: boolean) => {
if (agyAckBypassSavingRef.current || agyAckBypassSaving || saving) return;
if (nextValue) {
const confirmed = window.confirm(
'Enable AGY power user mode?\n\nThis skips AGY safety prompts. You accept the OAuth risk.'
);
if (!confirmed) return;
}
try {
agyAckBypassSavingRef.current = true;
setAgyAckBypassSaving(true);
const response = await fetch('/api/settings/auth/antigravity-risk', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ antigravityAckBypass: nextValue }),
});
if (!response.ok) {
const data = (await response.json()) as { error?: string };
throw new Error(data.error || 'Failed to update AGY power user mode');
}
setAgyAckBypass(nextValue);
toast.success(nextValue ? 'AGY power user mode enabled.' : 'AGY power user mode disabled.');
await fetchRawConfig();
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to update AGY power user mode');
} finally {
agyAckBypassSavingRef.current = false;
setAgyAckBypassSaving(false);
}
},
[agyAckBypassSaving, fetchRawConfig, saving]
);
// Backend state (loaded from API) + mutation hook for proper query invalidation
const [backend, setBackend] = useState<'original' | 'plus'>('plus');
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
@@ -138,12 +199,13 @@ export default function ProxySection() {
// Load data on mount
useEffect(() => {
fetchConfig();
void fetchAgyAckBypass();
fetchRawConfig();
// eslint-disable-next-line react-hooks/set-state-in-effect -- Async data fetching on mount is intended
void fetchBackend();
void checkPlusOnlyVariants();
}, [fetchConfig, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
}, [fetchConfig, fetchAgyAckBypass, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
if (loading || !config) {
return (
@@ -403,6 +465,41 @@ export default function ProxySection() {
)}
</div>
{/* Safety */}
<div className="space-y-3">
<h3 className="text-base font-medium flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-amber-700 dark:text-amber-300" />
Safety
</h3>
<div className="space-y-3 rounded-lg border border-amber-400/35 bg-amber-50/70 p-4 dark:border-amber-800/60 dark:bg-amber-950/25">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<p className="font-medium text-sm">Antigravity Power User Mode</p>
<p className="text-xs text-muted-foreground">
Skip AGY responsibility checklist in Add Account and `ccs agy` flows.
</p>
</div>
<Switch
aria-labelledby="agy-power-user-mode-label"
aria-describedby="agy-power-user-mode-description"
checked={agyAckBypass}
disabled={agyAckBypassLoading || agyAckBypassSaving || saving}
onCheckedChange={saveAgyAckBypass}
/>
</div>
<p
id="agy-power-user-mode-description"
className="text-xs text-amber-800/90 dark:text-amber-200/90"
>
Use only if you fully understand the OAuth suspension/ban risk pattern (#509). CCS
cannot assume responsibility for account loss.
</p>
<span id="agy-power-user-mode-label" className="sr-only">
Toggle AGY power user mode
</span>
</div>
</div>
{/* Remote Settings - Show when remote mode is enabled */}
{isRemoteMode && (
<RemoteProxyCard
@@ -517,11 +614,12 @@ export default function ProxySection() {
size="sm"
onClick={() => {
fetchConfig();
fetchAgyAckBypass();
fetchRawConfig();
fetchBackend();
checkPlusOnlyVariants();
}}
disabled={loading || saving}
disabled={loading || saving || agyAckBypassSaving}
className="w-full"
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />