fix(accounts-ui): condense sync guidance and remove redundant controls

This commit is contained in:
Tam Nhu Tran
2026-02-26 21:06:55 +07:00
parent 6cfc8d4a45
commit 286180e465
3 changed files with 116 additions and 264 deletions
+6 -35
View File
@@ -24,7 +24,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Check, CheckCheck, Link2, Pencil, RotateCcw, Trash2, Unlink } from 'lucide-react';
import { Check, CheckCheck, Pencil, RotateCcw, Trash2 } from 'lucide-react';
import { EditAccountContextDialog } from '@/components/account/edit-account-context-dialog';
import {
useSetDefaultAccount,
@@ -137,51 +137,22 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
deleteMutation.isPending ||
updateContextMutation.isPending;
const isCliproxy = row.original.type === 'cliproxy';
const isShared = row.original.context_mode === 'shared';
const hasLegacyInference =
row.original.context_inferred || row.original.continuity_inferred;
return (
<div className="flex items-center gap-1">
{!isCliproxy && (
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
disabled={isPending}
onClick={() => setContextTarget(row.original)}
title="Edit context mode"
>
<Pencil className="w-4 h-4" />
</Button>
)}
{!isCliproxy && (
<Button
variant="outline"
size="sm"
className="h-8 px-2"
disabled={isPending}
onClick={() =>
updateContextMutation.mutate({
name: row.original.name,
context_mode: isShared ? 'isolated' : 'shared',
context_group: isShared ? undefined : row.original.context_group || 'default',
continuity_mode: isShared ? undefined : 'standard',
})
}
title={isShared ? 'Switch to isolated mode' : 'Switch to shared mode'}
onClick={() => setContextTarget(row.original)}
title="Edit sync mode, group, and continuity depth"
>
{isShared ? (
<>
<Unlink className="w-3 h-3 mr-1" />
Unlink
</>
) : (
<>
<Link2 className="w-3 h-3 mr-1" />
Link
</>
)}
<Pencil className="w-3.5 h-3.5 mr-1" />
Sync
</Button>
)}
{!isCliproxy && hasLegacyInference && (
@@ -270,7 +241,7 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
created: 'w-[150px]',
last_used: 'w-[150px]',
context: 'w-[170px]',
actions: 'w-[340px]',
actions: 'w-[290px]',
}[header.id] || 'w-auto';
return (
@@ -1,6 +1,8 @@
import { useState } from 'react';
import {
ArrowRight,
ArrowRightLeft,
ChevronDown,
Layers3,
Link2,
Unlink,
@@ -8,7 +10,9 @@ import {
type LucideIcon,
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
interface HistorySyncLearningMapProps {
@@ -17,51 +21,49 @@ interface HistorySyncLearningMapProps {
deeperSharedCount: number;
sharedGroups: string[];
legacyTargetCount: number;
cliproxyCount: number;
}
type StageTone = 'isolated' | 'shared' | 'deeper';
function StageCard({
function StageTile({
title,
count,
icon: Icon,
tone,
description,
}: {
title: string;
count: number;
icon: LucideIcon;
tone: StageTone;
description: string;
}) {
const toneClasses: Record<StageTone, { card: string; icon: string; count: string }> = {
const toneClasses: Record<StageTone, { border: string; icon: string; count: string }> = {
isolated: {
card: 'border-blue-300/60 bg-blue-50/40 dark:border-blue-900/40 dark:bg-blue-900/10',
border: 'border-blue-300/60 bg-blue-50/40 dark:border-blue-900/40 dark:bg-blue-900/10',
icon: 'text-blue-700 dark:text-blue-400',
count: 'text-blue-700 dark:text-blue-400',
},
shared: {
card: 'border-emerald-300/60 bg-emerald-50/40 dark:border-emerald-900/40 dark:bg-emerald-900/10',
border:
'border-emerald-300/60 bg-emerald-50/40 dark:border-emerald-900/40 dark:bg-emerald-900/10',
icon: 'text-emerald-700 dark:text-emerald-400',
count: 'text-emerald-700 dark:text-emerald-400',
},
deeper: {
card: 'border-indigo-300/60 bg-indigo-50/40 dark:border-indigo-900/40 dark:bg-indigo-900/10',
border:
'border-indigo-300/60 bg-indigo-50/40 dark:border-indigo-900/40 dark:bg-indigo-900/10',
icon: 'text-indigo-700 dark:text-indigo-400',
count: 'text-indigo-700 dark:text-indigo-400',
},
};
return (
<div className={cn('rounded-md border p-3', toneClasses[tone].card)}>
<div className={cn('rounded-md border p-2.5', toneClasses[tone].border)}>
<div className="flex items-center justify-between gap-2">
<p className="text-sm font-semibold">{title}</p>
<Icon className={cn('h-4 w-4', toneClasses[tone].icon)} />
<p className="text-xs font-semibold">{title}</p>
<Icon className={cn('h-3.5 w-3.5', toneClasses[tone].icon)} />
</div>
<p className={cn('mt-1 text-2xl font-mono font-semibold', toneClasses[tone].count)}>
{count}
</p>
<p className="mt-1 text-xs text-muted-foreground">{description}</p>
<p className={cn('mt-1 text-lg font-mono font-semibold', toneClasses[tone].count)}>{count}</p>
</div>
);
}
@@ -72,91 +74,93 @@ export function HistorySyncLearningMap({
deeperSharedCount,
sharedGroups,
legacyTargetCount,
cliproxyCount,
}: HistorySyncLearningMapProps) {
const [open, setOpen] = useState(false);
const groupsToShow = sharedGroups.length > 0 ? sharedGroups : ['default'];
return (
<Card className="border-dashed">
<CardHeader className="pb-3">
<CardHeader className="pb-2">
<div className="flex items-center justify-between gap-2">
<CardTitle className="text-base">How History Sync Works</CardTitle>
<div>
<CardTitle className="text-base">How History Sync Works</CardTitle>
<CardDescription className="mt-1">
Isolated -&gt; Shared -&gt; Deeper. Use <code>Sync</code> per row for all changes.
</CardDescription>
</div>
<Badge variant="outline">Learning Map</Badge>
</div>
<CardDescription>
Accounts can move between modes at any time. Link/unlink is instant; Edit gives full
control of group and continuity depth.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 lg:grid-cols-[1fr_auto_1fr_auto_1fr] lg:items-center">
<StageCard
title="Isolated"
count={isolatedCount}
icon={Unlink}
tone="isolated"
description="No shared history with other accounts."
/>
<div className="hidden lg:flex justify-center text-muted-foreground">
<ArrowRight className="h-4 w-4" />
</div>
<StageCard
title="Shared (Standard)"
count={sharedStandardCount}
icon={Link2}
tone="shared"
description="Shared project context only."
/>
<div className="hidden lg:flex justify-center text-muted-foreground">
<ArrowRight className="h-4 w-4" />
</div>
<StageCard
title="Shared (Deeper)"
count={deeperSharedCount}
icon={Waves}
tone="deeper"
description="Adds continuity data: session-env/file-history/todos/shell-snapshots."
/>
</div>
<div className="grid gap-3 lg:grid-cols-2">
<div className="rounded-md border bg-muted/20 p-3">
<div className="flex items-center gap-2">
<ArrowRightLeft className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-semibold">Mode Switch Actions</p>
</div>
<div className="mt-2 flex flex-wrap gap-2">
<Badge variant="secondary">Link: Isolated -&gt; Shared</Badge>
<Badge variant="secondary">Unlink: Shared -&gt; Isolated</Badge>
<Badge variant="secondary">Edit: Group + Deeper</Badge>
</div>
</div>
<div className="rounded-md border bg-muted/20 p-3">
<div className="flex items-center gap-2">
<Layers3 className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-semibold">History Sync Group</p>
</div>
<p className="mt-1 text-xs text-muted-foreground">
Accounts in the same group share the same project context bucket. Group names are
user-defined lanes, with <code>default</code> as fallback.
</p>
<div className="mt-2 flex flex-wrap gap-2">
{groupsToShow.map((group) => (
<Badge key={group} variant="outline" className="font-mono text-[11px]">
{group}
</Badge>
))}
</div>
</div>
</div>
{legacyTargetCount > 0 && (
<div className="rounded-md border border-amber-500/50 bg-amber-500/10 px-3 py-2 text-xs text-amber-800 dark:text-amber-300">
{legacyTargetCount} legacy account
{legacyTargetCount > 1 ? 's still need' : ' still needs'} explicit confirmation. Use{' '}
<strong>Confirm Legacy Policies</strong> in Action Center.
<CardContent className="space-y-3">
{cliproxyCount > 0 && (
<div className="rounded-md border border-blue-300/60 bg-blue-50/40 px-3 py-2 text-xs text-blue-800 dark:border-blue-900/40 dark:bg-blue-900/10 dark:text-blue-300">
{cliproxyCount} CLIProxy Claude pool account{cliproxyCount > 1 ? 's are' : ' is'}
managed in Action Center / CLIProxy page.
</div>
)}
<div className="grid gap-2 sm:grid-cols-[1fr_auto_1fr_auto_1fr] sm:items-center">
<StageTile title="Isolated" count={isolatedCount} icon={Unlink} tone="isolated" />
<div className="hidden sm:flex justify-center text-muted-foreground">
<ArrowRight className="h-4 w-4" />
</div>
<StageTile title="Shared" count={sharedStandardCount} icon={Link2} tone="shared" />
<div className="hidden sm:flex justify-center text-muted-foreground">
<ArrowRight className="h-4 w-4" />
</div>
<StageTile title="Deeper" count={deeperSharedCount} icon={Waves} tone="deeper" />
</div>
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger asChild>
<Button variant="ghost" className="h-auto w-full justify-between px-0 py-0 text-xs">
<span className="text-muted-foreground">
Show details: groups, switching, and legacy policy
</span>
<ChevronDown className={cn('h-4 w-4 transition-transform', open && 'rotate-180')} />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="pt-2">
<div className="grid gap-2 lg:grid-cols-2">
<div className="rounded-md border bg-muted/20 p-2.5 text-xs">
<div className="flex items-center gap-2">
<ArrowRightLeft className="h-3.5 w-3.5 text-muted-foreground" />
<p className="font-semibold">Mode Switch</p>
</div>
<p className="mt-1 text-muted-foreground">
Sync dialog lets users move between isolated/shared and choose deeper continuity.
</p>
</div>
<div className="rounded-md border bg-muted/20 p-2.5 text-xs">
<div className="flex items-center gap-2">
<Layers3 className="h-3.5 w-3.5 text-muted-foreground" />
<p className="font-semibold">History Sync Group</p>
</div>
<p className="mt-1 text-muted-foreground">
Same group means shared project context lane. Default fallback is{' '}
<code>default</code>.
</p>
<div className="mt-2 flex flex-wrap gap-1.5">
{groupsToShow.map((group) => (
<Badge key={group} variant="outline" className="font-mono text-[10px]">
{group}
</Badge>
))}
</div>
</div>
</div>
{legacyTargetCount > 0 && (
<div className="mt-2 rounded-md border border-amber-500/50 bg-amber-500/10 px-3 py-2 text-xs text-amber-800 dark:text-amber-300">
{legacyTargetCount} legacy account
{legacyTargetCount > 1 ? 's still need' : ' still needs'} explicit confirmation.
</div>
)}
</CollapsibleContent>
</Collapsible>
</CardContent>
</Card>
);
+19 -142
View File
@@ -10,11 +10,8 @@ import {
ArrowRight,
ChevronDown,
ChevronLeft,
Link2,
Plus,
Unlink,
Users,
Waves,
Zap,
} from 'lucide-react';
import { AccountsTable } from '@/components/account/accounts-table';
@@ -23,82 +20,11 @@ import { HistorySyncLearningMap } from '@/components/account/history-sync-learni
import { CopyButton } from '@/components/ui/copy-button';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useAccounts, useConfirmLegacyAccountPolicies } from '@/hooks/use-accounts';
import { cn } from '@/lib/utils';
import type { LucideIcon } from 'lucide-react';
type MetricTone = 'default' | 'shared' | 'deeper' | 'isolated';
function MetricTile({
label,
value,
icon: Icon,
tone = 'default',
}: {
label: string;
value: number;
icon: LucideIcon;
tone?: MetricTone;
}) {
const toneClasses: Record<MetricTone, { border: string; icon: string }> = {
default: {
border: 'border-border',
icon: 'text-primary',
},
shared: {
border: 'border-emerald-300/60 dark:border-emerald-900/40',
icon: 'text-emerald-700 dark:text-emerald-400',
},
deeper: {
border: 'border-indigo-300/60 dark:border-indigo-900/40',
icon: 'text-indigo-700 dark:text-indigo-400',
},
isolated: {
border: 'border-blue-300/60 dark:border-blue-900/40',
icon: 'text-blue-700 dark:text-blue-400',
},
};
return (
<div className={cn('rounded-md border bg-card px-3 py-2.5', toneClasses[tone].border)}>
<div className="flex items-center justify-between gap-2">
<span className="text-[11px] uppercase tracking-wide text-muted-foreground">{label}</span>
<Icon className={cn('h-3.5 w-3.5', toneClasses[tone].icon)} />
</div>
<p className={cn('mt-1 text-xl font-mono font-semibold', toneClasses[tone].icon)}>{value}</p>
</div>
);
}
function StrategyCard({
title,
description,
variant,
}: {
title: string;
description: string;
variant: 'auth' | 'pool';
}) {
const variantClasses =
variant === 'auth'
? 'border-emerald-300/70 bg-emerald-50/40 dark:border-emerald-900/40 dark:bg-emerald-900/10'
: 'border-blue-300/70 bg-blue-50/40 dark:border-blue-900/40 dark:bg-blue-900/10';
const titleClasses =
variant === 'auth'
? 'text-emerald-800 dark:text-emerald-300'
: 'text-blue-800 dark:text-blue-300';
return (
<div className={cn('rounded-md border px-3 py-2.5', variantClasses)}>
<p className={cn('text-sm font-semibold', titleClasses)}>{title}</p>
<p className="mt-1 text-xs text-muted-foreground">{description}</p>
</div>
);
}
export function AccountsPage() {
const navigate = useNavigate();
@@ -164,40 +90,7 @@ export function AccountsPage() {
</div>
<ScrollArea className="flex-1">
<div className="p-3 space-y-4">
<section className="space-y-2">
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
Snapshot
</p>
<div className="grid grid-cols-2 gap-2">
<MetricTile label="Total" value={authAccounts.length} icon={Users} />
<MetricTile label="Shared" value={sharedCount} icon={Link2} tone="shared" />
<MetricTile label="Deeper" value={deeperSharedCount} icon={Waves} tone="deeper" />
<MetricTile
label="Isolated"
value={isolatedCount}
icon={Unlink}
tone="isolated"
/>
</div>
</section>
<section className="space-y-2">
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
Strategy Lanes
</p>
<StrategyCard
variant="auth"
title="Lane A: ccs auth continuity"
description="Isolation-first accounts with explicit shared/deeper controls per profile."
/>
<StrategyCard
variant="pool"
title="Lane B: CLIProxy Claude pool"
description="OAuth pool routing for lower manual account switching."
/>
</section>
<div className="p-3 space-y-3">
{hasLegacyFollowUp && (
<section className="space-y-2">
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
@@ -238,19 +131,14 @@ export function AccountsPage() {
</div>
</section>
)}
{!hasLegacyFollowUp && (
<div className="rounded-md border bg-background px-3 py-2 text-xs text-muted-foreground">
No legacy follow-up pending. Manage pool auth from Action Center.
</div>
)}
</div>
</ScrollArea>
<div className="p-3 border-t bg-background text-xs text-muted-foreground">
<div className="flex items-center justify-between">
<span>Standard shared</span>
<span className="font-mono">{sharedStandardCount}</span>
</div>
<div className="flex items-center justify-between mt-1">
<span>CLIProxy hidden</span>
<span className="font-mono">{cliproxyCount}</span>
</div>
</div>
</div>
{/* Main workspace */}
@@ -275,39 +163,26 @@ export function AccountsPage() {
This table is intentionally scoped to
<code className="mx-1 rounded bg-muted px-1 py-0.5">ccs auth</code>
accounts. Use
<code className="mx-1 rounded bg-muted px-1 py-0.5">Link</code>/
<code className="mx-1 rounded bg-muted px-1 py-0.5">Unlink</code>
for quick policy changes and pencil edit for advanced group/deeper settings.
<code className="mx-1 rounded bg-muted px-1 py-0.5">Sync</code>
for mode/group/depth changes.
</p>
</div>
<div className="flex-1 min-h-0 p-5 space-y-4 overflow-y-auto">
{cliproxyCount > 0 && (
<Alert variant="info">
<Zap className="h-4 w-4" />
<AlertTitle>CLIProxy pool accounts are managed in their own page</AlertTitle>
<AlertDescription>
{cliproxyCount} OAuth account{cliproxyCount > 1 ? 's are' : ' is'} available in
CLIProxy. This table only covers local
<code className="mx-1 rounded bg-muted px-1 py-0.5">ccs auth</code>
profiles.
</AlertDescription>
</Alert>
)}
<HistorySyncLearningMap
isolatedCount={isolatedCount}
sharedStandardCount={sharedStandardCount}
deeperSharedCount={deeperSharedCount}
sharedGroups={sharedGroups}
legacyTargetCount={legacyTargetCount}
cliproxyCount={cliproxyCount}
/>
<Card className="flex flex-col">
<CardHeader className="pb-3">
<CardTitle className="text-lg">Account Matrix</CardTitle>
<CardDescription>
Shared total: {sharedCount}. Actions now include quick link/unlink plus legacy
Shared total: {sharedCount}. Actions include Sync settings and legacy
confirmation.
</CardDescription>
</CardHeader>
@@ -493,12 +368,14 @@ export function AccountsPage() {
</CardContent>
</Card>
<div className="grid grid-cols-2 gap-2">
<MetricTile label="Total" value={authAccounts.length} icon={Users} />
<MetricTile label="Shared" value={sharedCount} icon={Link2} tone="shared" />
<MetricTile label="Deeper" value={deeperSharedCount} icon={Waves} tone="deeper" />
<MetricTile label="Isolated" value={isolatedCount} icon={Unlink} tone="isolated" />
</div>
<HistorySyncLearningMap
isolatedCount={isolatedCount}
sharedStandardCount={sharedStandardCount}
deeperSharedCount={deeperSharedCount}
sharedGroups={sharedGroups}
legacyTargetCount={legacyTargetCount}
cliproxyCount={cliproxyCount}
/>
<Card>
<CardHeader className="pb-3">