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" /> <ShieldAlert className="h-3.5 w-3.5" />
Power user mode enabled Power user mode enabled
</div> </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. responsibility for OAuth/account risk.
</div> </div>
)} )}
+10 -114
View File
@@ -3,12 +3,11 @@
* Settings section for CLIProxy auth tokens (API key and management secret) * 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 { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Switch } from '@/components/ui/switch';
import { import {
RefreshCw, RefreshCw,
CheckCircle2, CheckCircle2,
@@ -21,7 +20,6 @@ import {
Check, Check,
KeyRound, KeyRound,
ShieldCheck, ShieldCheck,
ShieldAlert,
Save, Save,
} from 'lucide-react'; } from 'lucide-react';
import { useRawConfig } from '../hooks'; import { useRawConfig } from '../hooks';
@@ -53,10 +51,6 @@ export default function AuthSection() {
const [editedSecret, setEditedSecret] = useState<string | null>(null); const [editedSecret, setEditedSecret] = useState<string | null>(null);
const [copiedApiKey, setCopiedApiKey] = useState(false); const [copiedApiKey, setCopiedApiKey] = useState(false);
const [copiedSecret, setCopiedSecret] = 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 // Fetch tokens
const fetchTokens = useCallback(async () => { 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 // Load on mount
useEffect(() => { useEffect(() => {
fetchTokens(); fetchTokens();
fetchAgyAckBypass();
fetchRawConfig(); fetchRawConfig();
}, [fetchTokens, fetchAgyAckBypass, fetchRawConfig]); }, [fetchTokens, fetchRawConfig]);
// Clear success after timeout // Clear success after timeout
useEffect(() => { useEffect(() => {
@@ -118,8 +95,6 @@ export default function AuthSection() {
// Save all changes // Save all changes
const saveChanges = async () => { const saveChanges = async () => {
if (agyAckBypassSaving) return;
const hasApiKeyChange = editedApiKey !== null && editedApiKey !== tokens?.apiKey.value; const hasApiKeyChange = editedApiKey !== null && editedApiKey !== tokens?.apiKey.value;
const hasSecretChange = const hasSecretChange =
editedSecret !== null && editedSecret !== tokens?.managementSecret.value; editedSecret !== null && editedSecret !== tokens?.managementSecret.value;
@@ -159,8 +134,6 @@ export default function AuthSection() {
// Regenerate management secret // Regenerate management secret
const regenerateSecret = async () => { const regenerateSecret = async () => {
if (agyAckBypassSaving) return;
try { try {
setSaving(true); setSaving(true);
setError(null); setError(null);
@@ -185,8 +158,6 @@ export default function AuthSection() {
// Reset to defaults // Reset to defaults
const resetToDefaults = async () => { const resetToDefaults = async () => {
if (agyAckBypassSaving) return;
try { try {
setSaving(true); setSaving(true);
setError(null); setError(null);
@@ -226,50 +197,11 @@ export default function AuthSection() {
setTimeout(() => setCopiedSecret(false), 2000); 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 () => { const refreshAll = async () => {
if (loading || saving || agyAckBypassSaving) return; if (loading || saving) return;
setError(null); setError(null);
setSuccess(null); setSuccess(null);
await Promise.all([fetchTokens(), fetchAgyAckBypass(), fetchRawConfig()]); await Promise.all([fetchTokens(), fetchRawConfig()]);
}; };
if (loading || !tokens) { if (loading || !tokens) {
@@ -344,7 +276,7 @@ export default function AuthSection() {
value={displayApiKey} value={displayApiKey}
onChange={(e) => setEditedApiKey(e.target.value)} onChange={(e) => setEditedApiKey(e.target.value)}
placeholder="API key" placeholder="API key"
disabled={saving || agyAckBypassSaving} disabled={saving}
className="pr-20 font-mono text-sm" className="pr-20 font-mono text-sm"
/> />
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1"> <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} value={displaySecret}
onChange={(e) => setEditedSecret(e.target.value)} onChange={(e) => setEditedSecret(e.target.value)}
placeholder="Management secret" placeholder="Management secret"
disabled={saving || agyAckBypassSaving} disabled={saving}
className="pr-20 font-mono text-sm" className="pr-20 font-mono text-sm"
/> />
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1"> <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" variant="outline"
size="sm" size="sm"
onClick={regenerateSecret} onClick={regenerateSecret}
disabled={saving || agyAckBypassSaving} disabled={saving}
title="Generate new secure secret" title="Generate new secure secret"
> >
<Sparkles className="w-4 h-4" /> <Sparkles className="w-4 h-4" />
@@ -434,48 +366,12 @@ export default function AuthSection() {
</div> </div>
</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"> <div className="pt-4 border-t">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={resetToDefaults} onClick={resetToDefaults}
disabled={ disabled={saving || (!tokens.apiKey.isCustom && !tokens.managementSecret.isCustom)}
saving ||
agyAckBypassSaving ||
(!tokens.apiKey.isCustom && !tokens.managementSecret.isCustom)
}
className="gap-2" className="gap-2"
> >
<RotateCcw className="w-4 h-4" /> <RotateCcw className="w-4 h-4" />
@@ -494,7 +390,7 @@ export default function AuthSection() {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={refreshAll} onClick={refreshAll}
disabled={loading || saving || agyAckBypassSaving} disabled={loading || saving}
className="flex-1" className="flex-1"
> >
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
@@ -504,7 +400,7 @@ export default function AuthSection() {
variant="default" variant="default"
size="sm" size="sm"
onClick={saveChanges} onClick={saveChanges}
disabled={!hasChanges || saving || agyAckBypassSaving} disabled={!hasChanges || saving}
className="flex-1" className="flex-1"
> >
<Save className={`w-4 h-4 mr-2 ${saving ? 'animate-pulse' : ''}`} /> <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) * 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 { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
@@ -17,6 +17,7 @@ import {
Bug, Bug,
Box, Box,
AlertTriangle, AlertTriangle,
ShieldAlert,
} from 'lucide-react'; } from 'lucide-react';
import { useProxyConfig, useRawConfig } from '../../hooks'; import { useProxyConfig, useRawConfig } from '../../hooks';
import { useUpdateBackend, useProxyStatus } from '@/hooks/use-cliproxy'; import { useUpdateBackend, useProxyStatus } from '@/hooks/use-cliproxy';
@@ -67,6 +68,10 @@ export default function ProxySection() {
return false; 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) => { const handleDebugModeChange = (enabled: boolean) => {
setDebugMode(enabled); 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 // Backend state (loaded from API) + mutation hook for proper query invalidation
const [backend, setBackend] = useState<'original' | 'plus'>('plus'); const [backend, setBackend] = useState<'original' | 'plus'>('plus');
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false); const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
@@ -138,12 +199,13 @@ export default function ProxySection() {
// Load data on mount // Load data on mount
useEffect(() => { useEffect(() => {
fetchConfig(); fetchConfig();
void fetchAgyAckBypass();
fetchRawConfig(); fetchRawConfig();
// eslint-disable-next-line react-hooks/set-state-in-effect -- Async data fetching on mount is intended
void fetchBackend(); void fetchBackend();
void checkPlusOnlyVariants(); void checkPlusOnlyVariants();
}, [fetchConfig, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]); }, [fetchConfig, fetchAgyAckBypass, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
if (loading || !config) { if (loading || !config) {
return ( return (
@@ -403,6 +465,41 @@ export default function ProxySection() {
)} )}
</div> </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 */} {/* Remote Settings - Show when remote mode is enabled */}
{isRemoteMode && ( {isRemoteMode && (
<RemoteProxyCard <RemoteProxyCard
@@ -517,11 +614,12 @@ export default function ProxySection() {
size="sm" size="sm"
onClick={() => { onClick={() => {
fetchConfig(); fetchConfig();
fetchAgyAckBypass();
fetchRawConfig(); fetchRawConfig();
fetchBackend(); fetchBackend();
checkPlusOnlyVariants(); checkPlusOnlyVariants();
}} }}
disabled={loading || saving} disabled={loading || saving || agyAckBypassSaving}
className="w-full" className="w-full"
> >
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />