diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index 032a7cf8..dc10a280 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -4,6 +4,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from 'sonner';
import { queryClient } from '@/lib/query-client';
import { ThemeProvider } from '@/components/theme-provider';
+import { PrivacyProvider } from '@/contexts/privacy-context';
import { Layout } from '@/components/layout';
// Eager load: HomePage (initial route)
@@ -33,22 +34,24 @@ export default function App() {
return (
-
-
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
-
+
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
);
diff --git a/ui/src/components/account-flow-viz.tsx b/ui/src/components/account-flow-viz.tsx
index c23e95ea..05fa86d4 100644
--- a/ui/src/components/account-flow-viz.tsx
+++ b/ui/src/components/account-flow-viz.tsx
@@ -9,6 +9,7 @@ import { cn } from '@/lib/utils';
import { ProviderIcon } from '@/components/provider-icon';
import { PROVIDER_COLORS } from '@/lib/provider-config';
import { STATUS_COLORS } from '@/lib/utils';
+import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import {
ChevronRight,
X,
@@ -94,7 +95,13 @@ function formatTimelineTime(date: Date): string {
}
/** Connection Timeline Component - right sidebar panel */
-function ConnectionTimeline({ events }: { events: ConnectionEvent[] }) {
+function ConnectionTimeline({
+ events,
+ privacyMode,
+}: {
+ events: ConnectionEvent[];
+ privacyMode: boolean;
+}) {
if (events.length === 0) {
return (
@@ -149,7 +156,12 @@ function ConnectionTimeline({ events }: { events: ConnectionEvent[] }) {
{/* Event content */}
-
+
{cleanEmail(event.accountEmail)}
@@ -217,6 +229,9 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
const [selectedAccount, setSelectedAccount] = useState(null);
const [paths, setPaths] = useState([]);
+ // Privacy mode for demo purposes
+ const { privacyMode } = usePrivacy();
+
// Drag state for all cards (account IDs + 'provider')
const [dragOffsets, setDragOffsets] = useState>({});
const [draggingId, setDraggingId] = useState(null);
@@ -494,7 +509,12 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
{/* Drag handle indicator */}
-
+
{cleanEmail(account.email)}
-
+
{cleanEmail(account.email)}
@@ -704,7 +729,7 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
{/* Right Section: Connection Timeline - Fixed compact width */}
-
+
@@ -733,7 +758,12 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
className="w-2 h-2 rounded-full"
style={{ backgroundColor: selectedAccount.color }}
/>
-
+
{cleanEmail(selectedAccount.email)}
diff --git a/ui/src/components/auth-monitor.tsx b/ui/src/components/auth-monitor.tsx
index 4f812050..f6bfccc1 100644
--- a/ui/src/components/auth-monitor.tsx
+++ b/ui/src/components/auth-monitor.tsx
@@ -12,6 +12,7 @@ import { getProviderDisplayName, PROVIDER_COLORS } from '@/lib/provider-config';
import { Skeleton } from '@/components/ui/skeleton';
import { ProviderIcon } from '@/components/provider-icon';
import { AccountFlowViz } from '@/components/account-flow-viz';
+import { usePrivacy } from '@/contexts/privacy-context';
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
import { Activity, CheckCircle2, XCircle, ChevronRight, Radio } from 'lucide-react';
@@ -114,6 +115,7 @@ function InlineStatsBadge({ success, failure }: { success: number; failure: numb
export function AuthMonitor() {
const { data, isLoading, error } = useCliproxyAuth();
const { data: statsData, isLoading: statsLoading, dataUpdatedAt } = useCliproxyStats();
+ const { privacyMode } = usePrivacy();
const [selectedProvider, setSelectedProvider] = useState
(null);
const [hoveredProvider, setHoveredProvider] = useState(null);
const [timeSinceUpdate, setTimeSinceUpdate] = useState('');
@@ -406,7 +408,7 @@ export function AuthMonitor() {
key={acc.id}
className="w-2 h-2 rounded-full"
style={{ backgroundColor: acc.color }}
- title={cleanEmail(acc.email)}
+ title={privacyMode ? '••••••' : cleanEmail(acc.email)}
/>
))}
{ps.accounts.length > 5 && (
diff --git a/ui/src/components/cliproxy-dialog.tsx b/ui/src/components/cliproxy-dialog.tsx
index e91c1755..f699305d 100644
--- a/ui/src/components/cliproxy-dialog.tsx
+++ b/ui/src/components/cliproxy-dialog.tsx
@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy';
+import { usePrivacy } from '@/contexts/privacy-context';
const providers = ['gemini', 'codex', 'agy', 'qwen', 'iflow'] as const;
@@ -43,6 +44,7 @@ const providerOptions = [
export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
const createMutation = useCreateVariant();
const { data: authData } = useCliproxyAuth();
+ const { privacyMode } = usePrivacy();
const {
register,
@@ -115,7 +117,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
{providerAccounts.map((acc) => (
))}
diff --git a/ui/src/components/cliproxy/overview/credential-health-list.tsx b/ui/src/components/cliproxy/overview/credential-health-list.tsx
index a1833739..a1876ee9 100644
--- a/ui/src/components/cliproxy/overview/credential-health-list.tsx
+++ b/ui/src/components/cliproxy/overview/credential-health-list.tsx
@@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge';
import { CheckCircle2, AlertCircle, XCircle, MinusCircle, RefreshCw } from 'lucide-react';
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
import { cn } from '@/lib/utils';
+import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
type CredentialStatus = 'ready' | 'warning' | 'error' | 'disabled';
@@ -20,6 +21,7 @@ interface CredentialRowProps {
email?: string;
expiresAt?: string;
onRefresh?: () => void;
+ privacyMode?: boolean;
}
function CredentialRow({
@@ -30,6 +32,7 @@ function CredentialRow({
email,
expiresAt,
onRefresh,
+ privacyMode,
}: CredentialRowProps) {
const statusConfig = {
ready: {
@@ -76,7 +79,9 @@ function CredentialRow({
-
{email ?? name}
+
+ {email ?? name}
+
{provider}
@@ -124,6 +129,7 @@ function CredentialHealthSkeleton() {
export function CredentialHealthList() {
const { data: authData, isLoading } = useCliproxyAuth();
+ const { privacyMode } = usePrivacy();
if (isLoading) {
return ;
@@ -164,7 +170,11 @@ export function CredentialHealthList() {
{credentials.map((cred, i) => (
-
+
))}
diff --git a/ui/src/components/cliproxy/provider-editor.tsx b/ui/src/components/cliproxy/provider-editor.tsx
index 8d76b44d..d69eb1ae 100644
--- a/ui/src/components/cliproxy/provider-editor.tsx
+++ b/ui/src/components/cliproxy/provider-editor.tsx
@@ -57,6 +57,7 @@ import {
useDeletePreset,
} from '@/hooks/use-cliproxy';
import { cn } from '@/lib/utils';
+import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
// Lazy load CodeEditor
const CodeEditor = lazy(() =>
@@ -100,6 +101,7 @@ export function ProviderEditor({
const [conflictDialog, setConflictDialog] = useState(false);
const [customPresetOpen, setCustomPresetOpen] = useState(false);
const queryClient = useQueryClient();
+ const { privacyMode } = usePrivacy();
// Fetch available models from CLIProxy API
const { data: modelsData } = useCliproxyModels();
@@ -433,6 +435,7 @@ export function ProviderEditor({
onSetDefault={() => onSetDefault(account.id)}
onRemove={() => onRemoveAccount(account.id)}
isRemoving={isRemovingAccount}
+ privacyMode={privacyMode}
/>
))}
@@ -685,11 +688,13 @@ function AccountItem({
onSetDefault,
onRemove,
isRemoving,
+ privacyMode,
}: {
account: OAuthAccount;
onSetDefault: () => void;
onRemove: () => void;
isRemoving?: boolean;
+ privacyMode?: boolean;
}) {
return (
-
{account.email || account.id}
+
+ {account.email || account.id}
+
{account.isDefault && (
diff --git a/ui/src/components/layout.tsx b/ui/src/components/layout.tsx
index 2872d377..e7f841d7 100644
--- a/ui/src/components/layout.tsx
+++ b/ui/src/components/layout.tsx
@@ -3,6 +3,7 @@ import { Outlet } from 'react-router-dom';
import { SidebarProvider } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/app-sidebar';
import { ThemeToggle } from '@/components/theme-toggle';
+import { PrivacyToggle } from '@/components/privacy-toggle';
import { GitHubLink } from '@/components/github-link';
import { DocsLink } from '@/components/docs-link';
import { ConnectionIndicator } from '@/components/connection-indicator';
@@ -29,6 +30,7 @@ export function Layout() {
+
diff --git a/ui/src/components/privacy-toggle.tsx b/ui/src/components/privacy-toggle.tsx
new file mode 100644
index 00000000..eb70362d
--- /dev/null
+++ b/ui/src/components/privacy-toggle.tsx
@@ -0,0 +1,39 @@
+/**
+ * Privacy Toggle Button
+ * Toggles demo mode to blur personal information (emails, account IDs)
+ */
+
+import { Button } from '@/components/ui/button';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
+import { usePrivacy } from '@/contexts/privacy-context';
+import { Eye, EyeOff } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+export function PrivacyToggle() {
+ const { privacyMode, togglePrivacyMode } = usePrivacy();
+
+ return (
+
+
+
+
+
+
+ {privacyMode
+ ? 'Privacy mode ON - Click to show data'
+ : 'Privacy mode OFF - Click to hide data'}
+
+
+
+ );
+}
diff --git a/ui/src/components/quick-setup-wizard.tsx b/ui/src/components/quick-setup-wizard.tsx
index dfc3d6b4..1fc36d70 100644
--- a/ui/src/components/quick-setup-wizard.tsx
+++ b/ui/src/components/quick-setup-wizard.tsx
@@ -39,6 +39,8 @@ import {
import { useCliproxyAuth, useCreateVariant, useStartAuth } from '@/hooks/use-cliproxy';
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
+import { cn } from '@/lib/utils';
+import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface QuickSetupWizardProps {
open: boolean;
@@ -68,6 +70,7 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
const { data: authData, refetch } = useCliproxyAuth();
const createMutation = useCreateVariant();
const startAuthMutation = useStartAuth();
+ const { privacyMode } = usePrivacy();
// Get auth status for selected provider
const providerAuth = authData?.authStatus.find(
@@ -347,7 +350,9 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
-
{acc.email || acc.id}
+
+ {acc.email || acc.id}
+
{acc.isDefault && (
Default account
)}
@@ -403,7 +408,12 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
{selectedAccount && (
- Using: {selectedAccount.email || selectedAccount.id}
+
+ Using:{' '}
+
+ {selectedAccount.email || selectedAccount.id}
+
+
)}
diff --git a/ui/src/contexts/privacy-context.tsx b/ui/src/contexts/privacy-context.tsx
new file mode 100644
index 00000000..98cc98fb
--- /dev/null
+++ b/ui/src/contexts/privacy-context.tsx
@@ -0,0 +1,54 @@
+/**
+ * Privacy Context - Global demo mode for censoring personal information
+ * Provides blur effect on emails, account IDs, and other PII
+ * Persists state to localStorage
+ */
+
+/* eslint-disable react-refresh/only-export-components */
+import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
+
+interface PrivacyContextValue {
+ /** Whether privacy/demo mode is enabled */
+ privacyMode: boolean;
+ /** Toggle privacy mode on/off */
+ togglePrivacyMode: () => void;
+}
+
+const PrivacyContext = createContext
(null);
+
+const STORAGE_KEY = 'ccs-privacy-mode';
+
+export function PrivacyProvider({ children }: { children: ReactNode }) {
+ const [privacyMode, setPrivacyMode] = useState(() => {
+ // Initialize from localStorage
+ if (typeof window !== 'undefined') {
+ return localStorage.getItem(STORAGE_KEY) === 'true';
+ }
+ return false;
+ });
+
+ // Persist to localStorage when changed
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY, String(privacyMode));
+ }, [privacyMode]);
+
+ const togglePrivacyMode = () => setPrivacyMode((prev) => !prev);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePrivacy() {
+ const context = useContext(PrivacyContext);
+ if (!context) {
+ throw new Error('usePrivacy must be used within a PrivacyProvider');
+ }
+ return context;
+}
+
+/** CSS class for blurring sensitive content */
+export const PRIVACY_BLUR_CLASS =
+ 'blur-[4px] select-none hover:blur-none transition-all duration-200';