fix(accounts-ui): add actionable continuity controls and claude deep-link

This commit is contained in:
Tam Nhu Tran
2026-02-26 20:49:23 +07:00
parent 12c7a218b7
commit a974efb8b1
4 changed files with 350 additions and 77 deletions
+68 -3
View File
@@ -24,12 +24,13 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Check, Pencil, Trash2, RotateCcw } from 'lucide-react';
import { Check, CheckCheck, Link2, Pencil, RotateCcw, Trash2, Unlink } from 'lucide-react';
import { EditAccountContextDialog } from '@/components/account/edit-account-context-dialog';
import {
useSetDefaultAccount,
useDeleteAccount,
useResetDefaultAccount,
useUpdateAccountContext,
} from '@/hooks/use-accounts';
import type { Account } from '@/lib/api-client';
@@ -42,6 +43,7 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
const setDefaultMutation = useSetDefaultAccount();
const deleteMutation = useDeleteAccount();
const resetDefaultMutation = useResetDefaultAccount();
const updateContextMutation = useUpdateAccountContext();
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
const [contextTarget, setContextTarget] = useState<Account | null>(null);
@@ -130,8 +132,14 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
size: 220,
cell: ({ row }) => {
const isDefault = row.original.name === defaultAccount;
const isPending = setDefaultMutation.isPending || deleteMutation.isPending;
const isPending =
setDefaultMutation.isPending ||
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">
@@ -147,6 +155,63 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
<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'}
>
{isShared ? (
<>
<Unlink className="w-3 h-3 mr-1" />
Unlink
</>
) : (
<>
<Link2 className="w-3 h-3 mr-1" />
Link
</>
)}
</Button>
)}
{!isCliproxy && hasLegacyInference && (
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-amber-700 hover:text-amber-700 hover:bg-amber-500/10 dark:text-amber-400 dark:hover:text-amber-400"
disabled={isPending}
onClick={() =>
updateContextMutation.mutate({
name: row.original.name,
context_mode: row.original.context_mode === 'shared' ? 'shared' : 'isolated',
context_group:
row.original.context_mode === 'shared'
? row.original.context_group || 'default'
: undefined,
continuity_mode:
row.original.context_mode === 'shared'
? row.original.continuity_mode === 'deeper'
? 'deeper'
: 'standard'
: undefined,
})
}
title="Confirm this legacy account's current mode as explicit"
>
<CheckCheck className="w-3 h-3 mr-1" />
Confirm
</Button>
)}
<Button
variant={isDefault ? 'secondary' : 'default'}
size="sm"
@@ -205,7 +270,7 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
created: 'w-[150px]',
last_used: 'w-[150px]',
context: 'w-[170px]',
actions: 'w-[220px]',
actions: 'w-[340px]',
}[header.id] || 'w-auto';
return (
+41
View File
@@ -136,3 +136,44 @@ export function useUpdateAccountContext() {
},
});
}
export function useConfirmLegacyAccountPolicies() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (accounts: Account[]) => {
const legacyTargets = accounts.filter(
(account) => account.context_inferred || account.continuity_inferred
);
for (const account of legacyTargets) {
const isShared = account.context_mode === 'shared';
await api.accounts.updateContext(account.name, {
context_mode: isShared ? 'shared' : 'isolated',
context_group: isShared ? account.context_group || 'default' : undefined,
continuity_mode: isShared
? account.continuity_mode === 'deeper'
? 'deeper'
: 'standard'
: undefined,
});
}
return { updatedCount: legacyTargets.length };
},
onSuccess: ({ updatedCount }) => {
queryClient.invalidateQueries({ queryKey: ['accounts'] });
if (updatedCount > 0) {
toast.success(
`Confirmed explicit sync mode for ${updatedCount} legacy account${updatedCount > 1 ? 's' : ''}`
);
return;
}
toast.info('No legacy accounts need confirmation');
},
onError: (error: Error) => {
toast.error(error.message);
},
});
}
+214 -71
View File
@@ -5,7 +5,18 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { AlertTriangle, ArrowRight, Link2, Plus, Unlink, Users, Waves, Zap } from 'lucide-react';
import {
AlertTriangle,
ArrowRight,
ChevronDown,
ChevronLeft,
Link2,
Plus,
Unlink,
Users,
Waves,
Zap,
} from 'lucide-react';
import { AccountsTable } from '@/components/account/accounts-table';
import { CreateAuthProfileDialog } from '@/components/account/create-auth-profile-dialog';
import { CopyButton } from '@/components/ui/copy-button';
@@ -13,8 +24,9 @@ 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 } from '@/hooks/use-accounts';
import { useAccounts, useConfirmLegacyAccountPolicies } from '@/hooks/use-accounts';
import { cn } from '@/lib/utils';
import type { LucideIcon } from 'lucide-react';
@@ -90,7 +102,11 @@ function StrategyCard({
export function AccountsPage() {
const navigate = useNavigate();
const { data, isLoading } = useAccounts();
const confirmLegacyMutation = useConfirmLegacyAccountPolicies();
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [showGuideRail, setShowGuideRail] = useState(true);
const [guideOpen, setGuideOpen] = useState(false);
const authAccounts = data?.accounts || [];
const cliproxyCount = data?.cliproxyCount || 0;
const legacyContextCount = data?.legacyContextCount || 0;
@@ -99,7 +115,16 @@ export function AccountsPage() {
const sharedStandardCount = data?.sharedStandardCount || 0;
const deeperSharedCount = data?.deeperSharedCount || 0;
const isolatedCount = data?.isolatedCount || 0;
const hasLegacyFollowUp = legacyContextCount > 0 || legacyContinuityCount > 0;
const legacyTargets = authAccounts.filter(
(account) => account.context_inferred || account.continuity_inferred
);
const legacyTargetCount = legacyTargets.length;
const hasLegacyFollowUp = legacyTargetCount > 0;
const handleOpenClaudePool = () => navigate('/cliproxy?provider=claude');
const handleOpenClaudePoolAuth = () => navigate('/cliproxy?provider=claude&action=auth');
const handleConfirmLegacy = () => confirmLegacyMutation.mutate(legacyTargets);
return (
<>
@@ -128,11 +153,20 @@ export function AccountsPage() {
variant="outline"
size="sm"
className="justify-start"
onClick={() => navigate('/cliproxy')}
onClick={handleOpenClaudePool}
>
Open CLIProxy Claude Pool
<ArrowRight className="w-4 h-4 ml-auto" />
</Button>
<Button
variant="outline"
size="sm"
className="justify-start"
onClick={handleOpenClaudePoolAuth}
>
Authenticate Claude in Pool
<Zap className="w-4 h-4 ml-auto" />
</Button>
</div>
</div>
@@ -176,7 +210,7 @@ export function AccountsPage() {
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
Migration Follow-up
</p>
<div className="rounded-md border border-amber-500/50 bg-amber-500/10 p-3">
<div className="rounded-md border border-amber-500/50 bg-amber-500/10 p-3 space-y-3">
<div className="flex items-start gap-2">
<AlertTriangle className="h-4 w-4 mt-0.5 text-amber-700 dark:text-amber-400 shrink-0" />
<div className="space-y-1 text-xs">
@@ -196,6 +230,18 @@ export function AccountsPage() {
)}
</div>
</div>
<Button
variant="secondary"
size="sm"
className="w-full justify-start"
onClick={handleConfirmLegacy}
disabled={confirmLegacyMutation.isPending || legacyTargetCount === 0}
>
{confirmLegacyMutation.isPending
? 'Confirming Legacy Accounts...'
: `Confirm Current Policy for ${legacyTargetCount} Legacy Account${legacyTargetCount > 1 ? 's' : ''}`}
</Button>
</div>
</section>
)}
@@ -222,19 +268,29 @@ export function AccountsPage() {
<div className="flex items-center gap-2">
<Badge variant="outline">ccs auth Workspace</Badge>
<Badge variant="secondary">History Sync Controls</Badge>
<Button
variant="outline"
size="sm"
className="ml-auto"
onClick={() => setShowGuideRail((prev) => !prev)}
>
{showGuideRail ? 'Hide Action Center' : 'Show Action Center'}
</Button>
</div>
<h2 className="mt-2 text-xl font-semibold tracking-tight">Auth Accounts</h2>
<p className="mt-1 text-sm text-muted-foreground">
This table is intentionally scoped to
<code className="mx-1 rounded bg-muted px-1 py-0.5">ccs auth</code>
accounts. Edit each account for isolated, shared-standard, or shared-deeper
continuity behavior.
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.
</p>
</div>
<div className="flex-1 min-h-0 p-5 overflow-auto">
<div className="flex-1 min-h-0 p-5 space-y-4 overflow-y-auto">
{cliproxyCount > 0 && (
<Alert variant="info" className="mb-4">
<Alert variant="info">
<Zap className="h-4 w-4" />
<AlertTitle>CLIProxy pool accounts are managed in their own page</AlertTitle>
<AlertDescription>
@@ -246,15 +302,15 @@ export function AccountsPage() {
</Alert>
)}
<Card className="h-full flex flex-col">
<Card className="flex flex-col">
<CardHeader className="pb-3">
<CardTitle className="text-lg">Account Matrix</CardTitle>
<CardDescription>
Shared total: {sharedCount}. Update sync behavior from the pencil action in each
row.
Shared total: {sharedCount}. Actions now include quick link/unlink plus legacy
confirmation.
</CardDescription>
</CardHeader>
<CardContent className="flex-1 min-h-0">
<CardContent>
{isLoading ? (
<div className="text-muted-foreground">Loading accounts...</div>
) : (
@@ -265,64 +321,137 @@ export function AccountsPage() {
</div>
</div>
{/* Right guidance column */}
<div className="w-80 shrink-0 flex flex-col bg-muted/20">
<div className="p-4 border-b bg-background">
<h3 className="font-semibold">Continuity Guide</h3>
<p className="mt-1 text-xs text-muted-foreground">
Choose the lightest mode that solves your workflow.
</p>
</div>
<ScrollArea className="flex-1">
<div className="p-4 space-y-3">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Shared Standard</CardTitle>
<CardDescription>Project workspace sync only.</CardDescription>
</CardHeader>
<CardContent className="text-xs text-muted-foreground">
Best default when users need continuity but want minimal coupling.
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Shared Deeper (Advanced)</CardTitle>
<CardDescription>
Adds <code>session-env</code>, <code>file-history</code>,{' '}
<code>shell-snapshots</code>, <code>todos</code>.
</CardDescription>
</CardHeader>
<CardContent className="text-xs text-muted-foreground">
Use only when cross-account continuity is worth stronger coupling.
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Quick Commands</CardTitle>
<CardDescription>Copy and run in terminal.</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
<span className="flex-1 break-all">
ccs auth create work --context-group sprint-a --deeper-continuity
</span>
<CopyButton
value="ccs auth create work --context-group sprint-a --deeper-continuity"
size="icon"
/>
</div>
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
<span className="flex-1 break-all">ccs cliproxy auth claude</span>
<CopyButton value="ccs cliproxy auth claude" size="icon" />
</div>
</CardContent>
</Card>
{/* Right action center */}
{showGuideRail ? (
<div className="w-80 shrink-0 flex flex-col bg-muted/20">
<div className="p-4 border-b bg-background flex items-start justify-between gap-3">
<div>
<h3 className="font-semibold">Action Center</h3>
<p className="mt-1 text-xs text-muted-foreground">
High-value actions for pool auth and legacy cleanup.
</p>
</div>
<Button variant="ghost" size="icon" onClick={() => setShowGuideRail(false)}>
<ChevronLeft className="h-4 w-4" />
</Button>
</div>
</ScrollArea>
</div>
<ScrollArea className="flex-1">
<div className="p-4 space-y-3">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Immediate Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<Button
size="sm"
className="w-full justify-start"
onClick={handleOpenClaudePoolAuth}
>
<Zap className="w-4 h-4 mr-2" />
Authenticate Claude in Pool
</Button>
<Button
variant="outline"
size="sm"
className="w-full justify-start"
onClick={handleOpenClaudePool}
>
Open Claude Pool Settings
<ArrowRight className="w-4 h-4 ml-auto" />
</Button>
<Button
variant="outline"
size="sm"
className="w-full justify-start"
onClick={handleConfirmLegacy}
disabled={confirmLegacyMutation.isPending || legacyTargetCount === 0}
>
{confirmLegacyMutation.isPending
? 'Confirming Legacy Policies...'
: `Confirm Legacy Policies (${legacyTargetCount})`}
</Button>
</CardContent>
</Card>
<Collapsible open={guideOpen} onOpenChange={setGuideOpen}>
<Card>
<CardHeader className="pb-2">
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="h-auto w-full justify-between px-0 py-0"
>
<div className="text-left">
<CardTitle className="text-sm">Continuity Guide</CardTitle>
<CardDescription className="mt-1">
Expand only when needed.
</CardDescription>
</div>
<ChevronDown
className={cn(
'h-4 w-4 transition-transform',
guideOpen && 'rotate-180'
)}
/>
</Button>
</CollapsibleTrigger>
</CardHeader>
<CollapsibleContent>
<CardContent className="space-y-3 text-xs text-muted-foreground">
<div className="rounded-md border p-2.5">
<p className="font-semibold text-foreground">Shared Standard</p>
<p className="mt-1">
Project workspace sync only. Best default for most teams.
</p>
</div>
<div className="rounded-md border p-2.5">
<p className="font-semibold text-foreground">Shared Deeper</p>
<p className="mt-1">
Adds <code>session-env</code>, <code>file-history</code>,{' '}
<code>shell-snapshots</code>, <code>todos</code>.
</p>
</div>
<div className="rounded-md border p-2.5">
<p className="font-semibold text-foreground">Isolated</p>
<p className="mt-1">No link. Best for strict separation.</p>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Quick Commands</CardTitle>
<CardDescription>Copy and run in terminal.</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
<span className="flex-1 break-all">
ccs auth create work --context-group sprint-a --deeper-continuity
</span>
<CopyButton
value="ccs auth create work --context-group sprint-a --deeper-continuity"
size="icon"
/>
</div>
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
<span className="flex-1 break-all">ccs cliproxy auth claude</span>
<CopyButton value="ccs cliproxy auth claude" size="icon" />
</div>
</CardContent>
</Card>
</div>
</ScrollArea>
</div>
) : (
<div className="w-14 shrink-0 border-l bg-muted/20 flex items-start justify-center pt-3">
<Button variant="ghost" size="icon" onClick={() => setShowGuideRail(true)}>
<ChevronLeft className="h-4 w-4 rotate-180" />
</Button>
</div>
)}
</div>
</div>
@@ -342,10 +471,24 @@ export function AccountsPage() {
<Plus className="w-4 h-4 mr-2" />
Create Account
</Button>
<Button variant="outline" className="w-full" onClick={() => navigate('/cliproxy')}>
<Button variant="outline" className="w-full" onClick={handleOpenClaudePool}>
Open CLIProxy Claude Pool
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
<Button variant="outline" className="w-full" onClick={handleOpenClaudePoolAuth}>
Authenticate Claude in Pool
<Zap className="w-4 h-4 ml-2" />
</Button>
<Button
variant="outline"
className="w-full"
onClick={handleConfirmLegacy}
disabled={confirmLegacyMutation.isPending || legacyTargetCount === 0}
>
{confirmLegacyMutation.isPending
? 'Confirming Legacy Policies...'
: `Confirm Legacy Policies (${legacyTargetCount})`}
</Button>
</CardContent>
</Card>
+27 -3
View File
@@ -4,7 +4,7 @@
* Right panel: Provider Editor with split-view (settings + code editor)
*/
import { useState, useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -32,6 +32,7 @@ import {
} from '@/hooks/use-cliproxy';
import type { AuthStatus, Variant } from '@/lib/api-client';
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
import { getProviderDisplayName, isValidProvider } from '@/lib/provider-config';
import { cn } from '@/lib/utils';
// Sidebar provider item
@@ -198,9 +199,14 @@ export function CliproxyPage() {
const deleteMutation = useDeleteVariant();
// Selection state: either a provider or a variant
// Initialize from localStorage if available
// Initialize from URL provider deep-link, fallback to localStorage.
const [selectedProvider, setSelectedProviderState] = useState<string | null>(() => {
if (typeof window !== 'undefined') {
const query = new URLSearchParams(window.location.search);
const queryProvider = query.get('provider')?.trim().toLowerCase();
if (queryProvider && isValidProvider(queryProvider)) {
return queryProvider;
}
return localStorage.getItem('cliproxy-selected-provider');
}
return null;
@@ -211,7 +217,25 @@ export function CliproxyPage() {
provider: string;
displayName: string;
isFirstAccount: boolean;
} | null>(null);
} | null>(() => {
if (typeof window === 'undefined') {
return null;
}
const query = new URLSearchParams(window.location.search);
const queryProvider = query.get('provider')?.trim().toLowerCase();
const action = query.get('action');
if (action !== 'auth' || !queryProvider || !isValidProvider(queryProvider)) {
return null;
}
return {
provider: queryProvider,
displayName: getProviderDisplayName(queryProvider),
isFirstAccount: false,
};
});
const providers = useMemo(() => authData?.authStatus || [], [authData?.authStatus]);
const isRemoteMode = authData?.source === 'remote';