mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
feat(accounts): clarify continuity and resume guidance
- add derived continuity readiness states for the accounts dashboard - explain same-group and deeper continuity requirements in the page and sync dialog - handle partial legacy-confirm updates without leaving stale UI state Refs #904
This commit is contained in:
@@ -33,23 +33,24 @@ import {
|
||||
useResetDefaultAccount,
|
||||
useUpdateAccountContext,
|
||||
} from '@/hooks/use-accounts';
|
||||
import type { Account } from '@/lib/api-client';
|
||||
import type { AuthAccountRow, SharedGroupSummary } from '@/lib/account-continuity';
|
||||
|
||||
interface AccountsTableProps {
|
||||
data: Account[];
|
||||
data: AuthAccountRow[];
|
||||
defaultAccount: string | null;
|
||||
groupSummaries: SharedGroupSummary[];
|
||||
}
|
||||
|
||||
export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
|
||||
export function AccountsTable({ data, defaultAccount, groupSummaries }: AccountsTableProps) {
|
||||
const { t } = useTranslation();
|
||||
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);
|
||||
const [contextTarget, setContextTarget] = useState<AuthAccountRow | null>(null);
|
||||
|
||||
const columns: ColumnDef<Account>[] = [
|
||||
const columns: ColumnDef<AuthAccountRow>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: t('accountsTable.name'),
|
||||
@@ -104,38 +105,43 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
|
||||
const mode = row.original.context_mode || 'isolated';
|
||||
if (mode === 'shared') {
|
||||
const group = row.original.context_group || 'default';
|
||||
if (row.original.continuity_mode === 'deeper') {
|
||||
return (
|
||||
<span className="text-muted-foreground">
|
||||
{t('accountsTable.sharedGroupDeeper', { group })}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (row.original.continuity_inferred) {
|
||||
return (
|
||||
<span className="text-amber-700 dark:text-amber-400">
|
||||
{t('accountsTable.sharedGroupLegacy', { group })}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const mainLabel =
|
||||
row.original.continuity_mode === 'deeper'
|
||||
? t('accountsTable.sharedGroupDeeper', { group })
|
||||
: row.original.continuity_inferred
|
||||
? t('accountsTable.sharedGroupLegacy', { group })
|
||||
: t('accountsTable.sharedGroupStandard', { group });
|
||||
const peerLabel =
|
||||
row.original.sameGroupPeerCount > 0
|
||||
? t('accountsTable.sameGroupPeerCount', { count: row.original.sameGroupPeerCount })
|
||||
: t('accountsTable.noSameGroupPeer');
|
||||
return (
|
||||
<span className="text-muted-foreground">
|
||||
{t('accountsTable.sharedGroupStandard', { group })}
|
||||
</span>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-muted-foreground">{mainLabel}</p>
|
||||
<p className="text-xs text-muted-foreground/80">{peerLabel}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (row.original.context_inferred) {
|
||||
return (
|
||||
<span className="text-amber-700 dark:text-amber-400">
|
||||
{t('accountsTable.isolatedLegacy')}
|
||||
</span>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400">
|
||||
{t('accountsTable.isolatedLegacy')}
|
||||
</p>
|
||||
<p className="text-xs text-amber-700/80 dark:text-amber-400/80">
|
||||
{t('accountsTable.legacyReview')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className="text-muted-foreground">{t('accountsTable.isolated')}</span>;
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-muted-foreground">{t('accountsTable.isolated')}</p>
|
||||
<p className="text-xs text-muted-foreground/80">{t('accountsTable.noHandoff')}</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -299,7 +305,11 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
|
||||
</div>
|
||||
|
||||
{contextTarget && (
|
||||
<EditAccountContextDialog account={contextTarget} onClose={() => setContextTarget(null)} />
|
||||
<EditAccountContextDialog
|
||||
account={contextTarget}
|
||||
groupSummaries={groupSummaries}
|
||||
onClose={() => setContextTarget(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Delete confirmation dialog */}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { AlertTriangle, CheckCircle2, Link2, ShieldAlert } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
interface ContinuityReadinessCardProps {
|
||||
totalAccounts: number;
|
||||
isolatedCount: number;
|
||||
sharedAloneCount: number;
|
||||
sharedPeerAccountCount: number;
|
||||
deeperReadyAccountCount: number;
|
||||
sharedPeerGroups: string[];
|
||||
deeperReadyGroups: string[];
|
||||
}
|
||||
|
||||
type ReadinessState =
|
||||
| 'single'
|
||||
| 'isolated'
|
||||
| 'shared-alone'
|
||||
| 'shared-standard'
|
||||
| 'partial'
|
||||
| 'ready';
|
||||
|
||||
export function ContinuityReadinessCard({
|
||||
totalAccounts,
|
||||
isolatedCount,
|
||||
sharedAloneCount,
|
||||
sharedPeerAccountCount,
|
||||
deeperReadyAccountCount,
|
||||
sharedPeerGroups,
|
||||
deeperReadyGroups,
|
||||
}: ContinuityReadinessCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const readiness: ReadinessState =
|
||||
totalAccounts < 2
|
||||
? 'single'
|
||||
: sharedPeerGroups.length === 0
|
||||
? isolatedCount === totalAccounts
|
||||
? 'isolated'
|
||||
: 'shared-alone'
|
||||
: deeperReadyGroups.length === 0
|
||||
? 'shared-standard'
|
||||
: isolatedCount > 0 ||
|
||||
sharedAloneCount > 0 ||
|
||||
deeperReadyAccountCount < sharedPeerAccountCount ||
|
||||
deeperReadyGroups.length < sharedPeerGroups.length
|
||||
? 'partial'
|
||||
: 'ready';
|
||||
|
||||
const highlightGroup = deeperReadyGroups[0] || sharedPeerGroups[0] || 'default';
|
||||
const icon =
|
||||
readiness === 'ready' ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
|
||||
) : readiness === 'shared-standard' ? (
|
||||
<Link2 className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
) : readiness === 'single' ? (
|
||||
<ShieldAlert className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
);
|
||||
const stepItems =
|
||||
readiness === 'single'
|
||||
? [
|
||||
t('continuityReadiness.singleSteps.addAccount'),
|
||||
t('continuityReadiness.singleSteps.sameGroupLater'),
|
||||
t('continuityReadiness.singleSteps.enableDeeperLater'),
|
||||
t('continuityReadiness.singleSteps.resumeOriginal'),
|
||||
]
|
||||
: [
|
||||
t('continuityReadiness.steps.syncBoth'),
|
||||
t('continuityReadiness.steps.sameGroup', { group: highlightGroup }),
|
||||
t('continuityReadiness.steps.enableDeeper'),
|
||||
t('continuityReadiness.steps.resumeOriginal'),
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-lg">{t('continuityReadiness.title')}</CardTitle>
|
||||
<CardDescription>{t('continuityReadiness.description')}</CardDescription>
|
||||
</div>
|
||||
<Badge variant={readiness === 'ready' ? 'default' : 'secondary'}>
|
||||
{t(`continuityReadiness.state.${readiness}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="rounded-md border bg-muted/20 p-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('continuityReadiness.metrics.isolated')}
|
||||
</p>
|
||||
<p className="mt-1 text-2xl font-semibold">{isolatedCount}</p>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/20 p-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('continuityReadiness.metrics.sharedPeers')}
|
||||
</p>
|
||||
<p className="mt-1 text-2xl font-semibold">{sharedPeerAccountCount}</p>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/20 p-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('continuityReadiness.metrics.deeperReady')}
|
||||
</p>
|
||||
<p className="mt-1 text-2xl font-semibold">{deeperReadyAccountCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/20 p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="mt-0.5">{icon}</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{t(`continuityReadiness.messages.${readiness}.title`, { group: highlightGroup })}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(`continuityReadiness.messages.${readiness}.description`, {
|
||||
group: highlightGroup,
|
||||
count: sharedAloneCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">{t('continuityReadiness.stepsTitle')}</p>
|
||||
<ol className="space-y-2 pl-5 text-sm text-muted-foreground">
|
||||
{stepItems.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Account } from '@/lib/api-client';
|
||||
import type { AuthAccountRow, SharedGroupSummary } from '@/lib/account-continuity';
|
||||
import { useUpdateAccountContext } from '@/hooks/use-accounts';
|
||||
|
||||
type ContextMode = 'isolated' | 'shared';
|
||||
@@ -28,11 +28,16 @@ const MAX_CONTEXT_GROUP_LENGTH = 64;
|
||||
const CONTEXT_GROUP_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
||||
|
||||
interface EditAccountContextDialogProps {
|
||||
account: Account;
|
||||
account: AuthAccountRow;
|
||||
groupSummaries: SharedGroupSummary[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function EditAccountContextDialog({ account, onClose }: EditAccountContextDialogProps) {
|
||||
export function EditAccountContextDialog({
|
||||
account,
|
||||
groupSummaries,
|
||||
onClose,
|
||||
}: EditAccountContextDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const updateContextMutation = useUpdateAccountContext();
|
||||
const [mode, setMode] = useState<ContextMode>(
|
||||
@@ -44,11 +49,35 @@ export function EditAccountContextDialog({ account, onClose }: EditAccountContex
|
||||
);
|
||||
|
||||
const normalizedGroup = useMemo(() => group.trim().toLowerCase().replace(/\s+/g, '-'), [group]);
|
||||
const matchingGroup = useMemo(
|
||||
() => groupSummaries.find((summary) => summary.group === normalizedGroup),
|
||||
[groupSummaries, normalizedGroup]
|
||||
);
|
||||
const isSharedGroupValid =
|
||||
normalizedGroup.length > 0 &&
|
||||
normalizedGroup.length <= MAX_CONTEXT_GROUP_LENGTH &&
|
||||
CONTEXT_GROUP_PATTERN.test(normalizedGroup);
|
||||
const canSubmit = mode === 'isolated' || isSharedGroupValid;
|
||||
const sameGroupPeerCount =
|
||||
mode === 'shared'
|
||||
? Math.max(
|
||||
(matchingGroup?.sharedCount ?? 0) -
|
||||
(account.context_mode === 'shared' && account.context_group === normalizedGroup
|
||||
? 1
|
||||
: 0),
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
const sameGroupDeeperPeerCount =
|
||||
mode === 'shared'
|
||||
? Math.max(
|
||||
(matchingGroup?.deeperCount ?? 0) -
|
||||
(account.continuity_mode === 'deeper' && account.context_group === normalizedGroup
|
||||
? 1
|
||||
: 0),
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
|
||||
const handleSave = () => {
|
||||
if (!canSubmit) {
|
||||
@@ -150,6 +179,36 @@ export function EditAccountContextDialog({ account, onClose }: EditAccountContex
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('editAccountContext.credentialsIsolated')}
|
||||
</p>
|
||||
|
||||
<div className="rounded-md border bg-muted/20 p-3 text-xs">
|
||||
<p className="font-medium text-foreground">
|
||||
{t('editAccountContext.implicationTitle')}
|
||||
</p>
|
||||
<div className="mt-2 space-y-1.5 text-muted-foreground">
|
||||
{mode === 'isolated' ? (
|
||||
<p>{t('editAccountContext.isolatedImplication')}</p>
|
||||
) : (
|
||||
<>
|
||||
<p>{t('editAccountContext.sameGroupRule', { group: normalizedGroup })}</p>
|
||||
<p>
|
||||
{sameGroupPeerCount > 0
|
||||
? t('editAccountContext.sameGroupPeerCount', { count: sameGroupPeerCount })
|
||||
: t('editAccountContext.noSameGroupPeer')}
|
||||
</p>
|
||||
<p>
|
||||
{continuityMode === 'deeper'
|
||||
? sameGroupDeeperPeerCount > 0
|
||||
? t('editAccountContext.deeperReady', {
|
||||
count: sameGroupDeeperPeerCount,
|
||||
})
|
||||
: t('editAccountContext.deeperNeedsPeers')
|
||||
: t('editAccountContext.standardWarning')}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<p>{t('editAccountContext.resumeOriginalWarning')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -21,6 +21,8 @@ interface HistorySyncLearningMapProps {
|
||||
sharedStandardCount: number;
|
||||
deeperSharedCount: number;
|
||||
sharedGroups: string[];
|
||||
sharedPeerGroups: string[];
|
||||
deeperReadyGroups: string[];
|
||||
legacyTargetCount: number;
|
||||
cliproxyCount: number;
|
||||
}
|
||||
@@ -74,12 +76,20 @@ export function HistorySyncLearningMap({
|
||||
sharedStandardCount,
|
||||
deeperSharedCount,
|
||||
sharedGroups,
|
||||
sharedPeerGroups,
|
||||
deeperReadyGroups,
|
||||
legacyTargetCount,
|
||||
cliproxyCount,
|
||||
}: HistorySyncLearningMapProps) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const groupsToShow = sharedGroups.length > 0 ? sharedGroups : ['default'];
|
||||
const highlightGroup = deeperReadyGroups[0] || sharedPeerGroups[0] || groupsToShow[0];
|
||||
const hasMixedState =
|
||||
deeperReadyGroups.length > 0 &&
|
||||
(isolatedCount > 0 ||
|
||||
sharedStandardCount > 0 ||
|
||||
sharedPeerGroups.length > deeperReadyGroups.length);
|
||||
|
||||
return (
|
||||
<Card className="border-dashed">
|
||||
@@ -129,6 +139,16 @@ export function HistorySyncLearningMap({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
||||
{sharedPeerGroups.length === 0
|
||||
? t('historySyncLearningMap.sameGroupRule')
|
||||
: deeperReadyGroups.length === 0
|
||||
? t('historySyncLearningMap.deeperRecommendation', { group: highlightGroup })
|
||||
: hasMixedState
|
||||
? t('historySyncLearningMap.partialGroup', { group: highlightGroup })
|
||||
: t('historySyncLearningMap.readyGroup', { group: highlightGroup })}
|
||||
</div>
|
||||
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api-client';
|
||||
import type { Account } from '@/lib/api-client';
|
||||
import {
|
||||
summarizeAuthAccountContinuity,
|
||||
type AuthAccountRow,
|
||||
type SharedGroupSummary,
|
||||
} from '@/lib/account-continuity';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export interface AuthAccountsView {
|
||||
accounts: Account[];
|
||||
accounts: AuthAccountRow[];
|
||||
default: string | null;
|
||||
cliproxyCount: number;
|
||||
legacyContextCount: number;
|
||||
@@ -18,6 +23,13 @@ export interface AuthAccountsView {
|
||||
sharedStandardCount: number;
|
||||
deeperSharedCount: number;
|
||||
isolatedCount: number;
|
||||
sharedAloneCount: number;
|
||||
sharedPeerAccountCount: number;
|
||||
deeperReadyAccountCount: number;
|
||||
sharedPeerGroups: string[];
|
||||
deeperReadyGroups: string[];
|
||||
sharedGroups: string[];
|
||||
groupSummaries: SharedGroupSummary[];
|
||||
}
|
||||
|
||||
export function useAccounts() {
|
||||
@@ -26,36 +38,29 @@ export function useAccounts() {
|
||||
queryFn: () => api.accounts.list(),
|
||||
select: (data): AuthAccountsView => {
|
||||
const authAccounts = data.accounts.filter((account) => account.type !== 'cliproxy');
|
||||
const continuity = summarizeAuthAccountContinuity(authAccounts);
|
||||
const cliproxyCount = data.accounts.length - authAccounts.length;
|
||||
const sharedCount = authAccounts.filter(
|
||||
(account) => account.context_mode === 'shared'
|
||||
).length;
|
||||
const deeperSharedCount = authAccounts.filter(
|
||||
(account) => account.context_mode === 'shared' && account.continuity_mode === 'deeper'
|
||||
).length;
|
||||
const sharedStandardCount = Math.max(sharedCount - deeperSharedCount, 0);
|
||||
const isolatedCount = authAccounts.length - sharedCount;
|
||||
const legacyContextCount = authAccounts.filter((account) => account.context_inferred).length;
|
||||
const legacyContinuityCount = authAccounts.filter(
|
||||
(account) =>
|
||||
account.context_mode === 'shared' &&
|
||||
account.continuity_mode !== 'deeper' &&
|
||||
account.continuity_inferred
|
||||
).length;
|
||||
const defaultAccount = authAccounts.some((account) => account.name === data.default)
|
||||
const defaultAccount = continuity.accounts.some((account) => account.name === data.default)
|
||||
? data.default
|
||||
: null;
|
||||
|
||||
return {
|
||||
accounts: authAccounts,
|
||||
accounts: continuity.accounts,
|
||||
default: defaultAccount,
|
||||
cliproxyCount,
|
||||
legacyContextCount,
|
||||
legacyContinuityCount,
|
||||
sharedCount,
|
||||
sharedStandardCount,
|
||||
deeperSharedCount,
|
||||
isolatedCount,
|
||||
legacyContextCount: continuity.legacyContextCount,
|
||||
legacyContinuityCount: continuity.legacyContinuityCount,
|
||||
sharedCount: continuity.sharedCount,
|
||||
sharedStandardCount: continuity.sharedStandardCount,
|
||||
deeperSharedCount: continuity.deeperSharedCount,
|
||||
isolatedCount: continuity.isolatedCount,
|
||||
sharedAloneCount: continuity.sharedAloneCount,
|
||||
sharedPeerAccountCount: continuity.sharedPeerAccountCount,
|
||||
deeperReadyAccountCount: continuity.deeperReadyAccountCount,
|
||||
sharedPeerGroups: continuity.sharedPeerGroups,
|
||||
deeperReadyGroups: continuity.deeperReadyGroups,
|
||||
sharedGroups: continuity.sharedGroups,
|
||||
groupSummaries: continuity.groupSummaries,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -146,23 +151,40 @@ export function useConfirmLegacyAccountPolicies() {
|
||||
(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,
|
||||
});
|
||||
const results = await Promise.allSettled(
|
||||
legacyTargets.map((account) => {
|
||||
const isShared = account.context_mode === 'shared';
|
||||
return 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,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const failed = results.filter((result) => result.status === 'rejected').length;
|
||||
return { updatedCount: legacyTargets.length - failed, failedCount: failed };
|
||||
},
|
||||
onSuccess: ({ updatedCount, failedCount }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['accounts'] });
|
||||
if (failedCount > 0 && updatedCount > 0) {
|
||||
toast.error(
|
||||
`Confirmed ${updatedCount} legacy account${updatedCount > 1 ? 's' : ''}, but ${failedCount} update${failedCount > 1 ? 's' : ''} failed. Refreshed account state.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (failedCount > 0) {
|
||||
toast.error(
|
||||
`Failed to confirm ${failedCount} legacy account${failedCount > 1 ? 's' : ''}. Refreshed account state.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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' : ''}`
|
||||
@@ -173,6 +195,7 @@ export function useConfirmLegacyAccountPolicies() {
|
||||
toast.info('No legacy accounts need confirmation');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['accounts'] });
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { Account } from '@/lib/api-client';
|
||||
|
||||
export interface SharedGroupSummary {
|
||||
group: string;
|
||||
sharedCount: number;
|
||||
deeperCount: number;
|
||||
accountNames: string[];
|
||||
}
|
||||
|
||||
export interface AuthAccountRow extends Account {
|
||||
sameGroupPeerCount: number;
|
||||
sameGroupDeeperPeerCount: number;
|
||||
}
|
||||
|
||||
export interface AuthAccountContinuitySummary {
|
||||
accounts: AuthAccountRow[];
|
||||
sharedCount: number;
|
||||
sharedStandardCount: number;
|
||||
deeperSharedCount: number;
|
||||
isolatedCount: number;
|
||||
legacyContextCount: number;
|
||||
legacyContinuityCount: number;
|
||||
sharedAloneCount: number;
|
||||
sharedPeerAccountCount: number;
|
||||
deeperReadyAccountCount: number;
|
||||
sharedPeerGroups: string[];
|
||||
deeperReadyGroups: string[];
|
||||
sharedGroups: string[];
|
||||
groupSummaries: SharedGroupSummary[];
|
||||
}
|
||||
|
||||
function sortGroups(groups: Iterable<string>): string[] {
|
||||
return Array.from(groups).sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function summarizeAuthAccountContinuity(accounts: Account[]): AuthAccountContinuitySummary {
|
||||
const groupMap = new Map<string, SharedGroupSummary>();
|
||||
|
||||
for (const account of accounts) {
|
||||
if (account.context_mode !== 'shared') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const group = account.context_group || 'default';
|
||||
const summary = groupMap.get(group) ?? {
|
||||
group,
|
||||
sharedCount: 0,
|
||||
deeperCount: 0,
|
||||
accountNames: [],
|
||||
};
|
||||
summary.sharedCount += 1;
|
||||
summary.accountNames.push(account.name);
|
||||
if (account.continuity_mode === 'deeper') {
|
||||
summary.deeperCount += 1;
|
||||
}
|
||||
groupMap.set(group, summary);
|
||||
}
|
||||
|
||||
const groupSummaries = Array.from(groupMap.values()).sort((left, right) =>
|
||||
left.group.localeCompare(right.group)
|
||||
);
|
||||
|
||||
const derivedAccounts: AuthAccountRow[] = accounts.map((account) => {
|
||||
if (account.context_mode !== 'shared') {
|
||||
return {
|
||||
...account,
|
||||
sameGroupPeerCount: 0,
|
||||
sameGroupDeeperPeerCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const group = account.context_group || 'default';
|
||||
const groupSummary = groupMap.get(group);
|
||||
const sameGroupPeerCount = Math.max((groupSummary?.sharedCount ?? 1) - 1, 0);
|
||||
const sameGroupDeeperPeerCount = Math.max(
|
||||
(groupSummary?.deeperCount ?? 0) - (account.continuity_mode === 'deeper' ? 1 : 0),
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
...account,
|
||||
sameGroupPeerCount,
|
||||
sameGroupDeeperPeerCount,
|
||||
};
|
||||
});
|
||||
|
||||
const sharedCount = derivedAccounts.filter((account) => account.context_mode === 'shared').length;
|
||||
const deeperSharedCount = derivedAccounts.filter(
|
||||
(account) => account.context_mode === 'shared' && account.continuity_mode === 'deeper'
|
||||
).length;
|
||||
const legacyContextCount = derivedAccounts.filter((account) => account.context_inferred).length;
|
||||
const legacyContinuityCount = derivedAccounts.filter(
|
||||
(account) =>
|
||||
account.context_mode === 'shared' &&
|
||||
account.continuity_mode !== 'deeper' &&
|
||||
account.continuity_inferred
|
||||
).length;
|
||||
|
||||
return {
|
||||
accounts: derivedAccounts,
|
||||
sharedCount,
|
||||
sharedStandardCount: Math.max(sharedCount - deeperSharedCount, 0),
|
||||
deeperSharedCount,
|
||||
isolatedCount: derivedAccounts.length - sharedCount,
|
||||
legacyContextCount,
|
||||
legacyContinuityCount,
|
||||
sharedAloneCount: derivedAccounts.filter(
|
||||
(account) => account.context_mode === 'shared' && account.sameGroupPeerCount === 0
|
||||
).length,
|
||||
sharedPeerAccountCount: derivedAccounts.filter((account) => account.sameGroupPeerCount > 0)
|
||||
.length,
|
||||
deeperReadyAccountCount: derivedAccounts.filter(
|
||||
(account) => account.continuity_mode === 'deeper' && account.sameGroupDeeperPeerCount > 0
|
||||
).length,
|
||||
sharedPeerGroups: sortGroups(
|
||||
groupSummaries.filter((group) => group.sharedCount >= 2).map((group) => group.group)
|
||||
),
|
||||
deeperReadyGroups: sortGroups(
|
||||
groupSummaries.filter((group) => group.deeperCount >= 2).map((group) => group.group)
|
||||
),
|
||||
sharedGroups: sortGroups(groupSummaries.map((group) => group.group)),
|
||||
groupSummaries,
|
||||
};
|
||||
}
|
||||
+414
-9
@@ -420,13 +420,32 @@ const resources = {
|
||||
'Advanced mode also syncs session-env, file-history, shell-snapshots, and todos.',
|
||||
standardHint: 'Standard mode syncs project workspace context only.',
|
||||
credentialsIsolated: 'Credentials and .anthropic remain isolated per account in all modes.',
|
||||
implicationTitle: 'What this means after save',
|
||||
isolatedImplication:
|
||||
'This account stays separate. Other accounts will not be able to resume its continuity state.',
|
||||
sameGroupRule:
|
||||
'Accounts must use the same group "{{group}}" before they can share continuity with each other.',
|
||||
noSameGroupPeer: 'No other account currently shares this group.',
|
||||
sameGroupPeerCount_one: '{{count}} other account already shares this group.',
|
||||
sameGroupPeerCount_other: '{{count}} other accounts already share this group.',
|
||||
deeperReady_one:
|
||||
'{{count}} same-group account already uses deeper continuity. This is the strongest available handoff setup.',
|
||||
deeperReady_other:
|
||||
'{{count}} same-group accounts already use deeper continuity. This is the strongest available handoff setup.',
|
||||
deeperNeedsPeers:
|
||||
'Deeper continuity is enabled here, but another account in this group still needs deeper continuity for stronger cross-account resume expectations.',
|
||||
standardWarning:
|
||||
'Standard shared mode is good for project context only. Cross-account resume expectations are stronger when both accounts use deeper continuity.',
|
||||
resumeOriginalWarning:
|
||||
'If the old conversation matters, resume it from the original account before you change the setup.',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
saving: 'Saving...',
|
||||
},
|
||||
historySyncLearningMap: {
|
||||
title: 'How History Sync Works',
|
||||
description: 'Isolated -> Shared -> Deeper. Use Sync per row for all changes.',
|
||||
description:
|
||||
'Isolated -> Shared -> Deeper. Cross-account resume needs the same group on both accounts.',
|
||||
learningMap: 'Learning Map',
|
||||
isolated: 'Isolated',
|
||||
shared: 'Shared',
|
||||
@@ -438,13 +457,92 @@ const resources = {
|
||||
showDetails: 'Show details: groups, switching, and legacy policy',
|
||||
modeSwitch: 'Mode Switch',
|
||||
modeSwitchDesc:
|
||||
'Sync dialog lets users move between isolated/shared and choose deeper continuity.',
|
||||
'Use Sync on each account to move between isolated/shared and choose deeper continuity.',
|
||||
historySyncGroup: 'History Sync Group',
|
||||
historySyncGroupDesc:
|
||||
'Same group means shared project context lane. Default fallback is default.',
|
||||
'Same group is required before accounts can share continuity. Default fallback is default.',
|
||||
sameGroupRule:
|
||||
'Right now, accounts only share continuity when both are shared in the same group.',
|
||||
deeperRecommendation:
|
||||
'Group "{{group}}" is shared, but deeper continuity is still the safer choice if users expect cross-account resume.',
|
||||
partialGroup:
|
||||
'Group "{{group}}" is in good shape, but other accounts or groups still need the same-group or deeper setup.',
|
||||
readyGroup:
|
||||
'Group "{{group}}" already has deeper continuity on multiple accounts. This is the strongest available handoff setup.',
|
||||
legacyConfirmation_one: '{{count}} legacy account still needs explicit confirmation.',
|
||||
legacyConfirmation_other: '{{count}} legacy accounts still need explicit confirmation.',
|
||||
},
|
||||
continuityReadiness: {
|
||||
title: 'Cross-Account Resume Check',
|
||||
description: 'Use this card to see what is missing before users switch accounts.',
|
||||
state: {
|
||||
single: 'single account',
|
||||
isolated: 'resume off',
|
||||
'shared-alone': 'shared but incomplete',
|
||||
'shared-standard': 'projects only',
|
||||
partial: 'partially ready',
|
||||
ready: 'stronger handoff ready',
|
||||
},
|
||||
metrics: {
|
||||
isolated: 'Isolated',
|
||||
sharedPeers: 'Shared With Peer',
|
||||
deeperReady: 'Deeper Ready',
|
||||
},
|
||||
messages: {
|
||||
single: {
|
||||
title: 'Only one auth account is configured.',
|
||||
description:
|
||||
'Cross-account handoff does not apply yet. Add another auth account before configuring shared continuity.',
|
||||
},
|
||||
isolated: {
|
||||
title: 'Cross-account resume is off right now.',
|
||||
description:
|
||||
'All visible accounts are isolated, so a session created in one account will stay with that account.',
|
||||
},
|
||||
'shared-alone': {
|
||||
title: 'Shared mode exists, but no group has a peer yet.',
|
||||
description_one:
|
||||
'{{count}} shared account is still waiting for another account in the same group.',
|
||||
description_other:
|
||||
'{{count}} shared accounts are still waiting for another account in the same group.',
|
||||
},
|
||||
'shared-standard': {
|
||||
title:
|
||||
'Group "{{group}}" shares project context, but deeper continuity is not paired yet.',
|
||||
description:
|
||||
'Users may still expect more than project sharing. Enable deeper continuity on both accounts in this group for stronger handoff expectations.',
|
||||
},
|
||||
partial: {
|
||||
title: 'One group is ready, but the overall setup is still mixed.',
|
||||
description:
|
||||
'At least one group already uses deeper continuity, but other accounts are still isolated, alone in a shared group, or missing deeper continuity.',
|
||||
},
|
||||
ready: {
|
||||
title: 'Group "{{group}}" has the strongest available continuity setup.',
|
||||
description:
|
||||
'Multiple accounts in this group already use deeper continuity. Resume behavior still depends on what Claude stores upstream.',
|
||||
},
|
||||
},
|
||||
stepsTitle: 'What users should do next',
|
||||
steps: {
|
||||
syncBoth: 'Open Sync on both accounts, not just the one you are switching into.',
|
||||
sameGroup:
|
||||
'Use the same History Sync Group on both accounts. "{{group}}" is the current recommended group.',
|
||||
enableDeeper:
|
||||
'If users expect cross-account resume instead of project-only sharing, turn on deeper continuity on both accounts.',
|
||||
resumeOriginal:
|
||||
'If the original conversation is important, resume it from the original account before changing continuity settings.',
|
||||
},
|
||||
singleSteps: {
|
||||
addAccount: 'Create a second ccs auth account before planning any cross-account handoff.',
|
||||
sameGroupLater:
|
||||
'When that second account exists, put both accounts in the same History Sync Group.',
|
||||
enableDeeperLater:
|
||||
'If users will expect cross-account resume instead of project-only sharing, enable deeper continuity on both accounts.',
|
||||
resumeOriginal:
|
||||
'If the original conversation is important, keep resuming it from the original account until the second account is ready.',
|
||||
},
|
||||
},
|
||||
accountsTable: {
|
||||
name: 'Name',
|
||||
type: 'Type',
|
||||
@@ -468,11 +566,16 @@ const resources = {
|
||||
'Are you sure you want to delete the account "{{name}}"? This will remove the profile and all its session data. This action cannot be undone.',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete',
|
||||
sharedGroupStandard: 'shared ({{group}}, standard)',
|
||||
sharedGroupDeeper: 'shared ({{group}}, deeper)',
|
||||
sharedGroupLegacy: 'shared ({{group}}, standard legacy)',
|
||||
sharedGroupStandard: 'shared ({{group}}, projects only)',
|
||||
sharedGroupDeeper: 'shared ({{group}}, deeper continuity)',
|
||||
sharedGroupLegacy: 'shared ({{group}}, projects only, legacy)',
|
||||
isolatedLegacy: 'isolated (legacy default)',
|
||||
isolated: 'isolated',
|
||||
noSameGroupPeer: 'No same-group peer yet',
|
||||
sameGroupPeerCount_one: '{{count}} same-group peer',
|
||||
sameGroupPeerCount_other: '{{count}} same-group peers',
|
||||
legacyReview: 'Review and confirm this inferred default.',
|
||||
noHandoff: 'Cross-account resume stays in the original account.',
|
||||
},
|
||||
addAccountDialog: {
|
||||
title: 'Add {{displayName}} Account',
|
||||
@@ -908,15 +1011,18 @@ const resources = {
|
||||
continuityGuide: 'Continuity Guide',
|
||||
expandWhenNeeded: 'Expand only when needed.',
|
||||
sharedStandard: 'Shared Standard',
|
||||
sharedStandardDesc: 'Project workspace sync only. Best default for most teams.',
|
||||
sharedStandardDesc:
|
||||
'Shared workspace only. Put both accounts in the same group before expecting any handoff.',
|
||||
sharedDeeper: 'Shared Deeper',
|
||||
sharedDeeperPrefix: 'Adds',
|
||||
sharedDeeperDesc:
|
||||
'Adds session-env, file-history, shell-snapshots, and todos. Recommended when users expect cross-account resume.',
|
||||
isolated: 'Isolated',
|
||||
isolatedDesc: 'No link. Best for strict separation.',
|
||||
isolatedDesc: 'No link. Conversations stay with the original account.',
|
||||
quickCommands: 'Quick Commands',
|
||||
quickCommandsDesc: 'Copy and run in terminal.',
|
||||
workspaceBadge: 'ccs auth Workspace',
|
||||
historySyncBadge: 'History Sync Controls',
|
||||
historySyncBadge: 'History & Resume Controls',
|
||||
authAccounts: 'Auth Accounts',
|
||||
tableScopePrefix: 'This table is intentionally scoped to',
|
||||
tableScopeMiddle: 'accounts. Use',
|
||||
@@ -1614,6 +1720,19 @@ const resources = {
|
||||
deeperHint: '高级模式还会同步 session-env、file-history、shell-snapshots 和 todos。',
|
||||
standardHint: '标准模式仅同步项目工作区上下文。',
|
||||
credentialsIsolated: '凭据与 .anthropic 在所有模式下均按账号隔离。',
|
||||
implicationTitle: '保存后会发生什么',
|
||||
isolatedImplication: '该账号会继续保持隔离,其他账号无法续接它的连续性状态。',
|
||||
sameGroupRule: '只有账号都使用同一个分组“{{group}}”时,才会彼此共享连续性。',
|
||||
noSameGroupPeer: '当前没有其他账号使用这个分组。',
|
||||
sameGroupPeerCount_one: '已有 {{count}} 个其他账号使用这个分组。',
|
||||
sameGroupPeerCount_other: '已有 {{count}} 个其他账号使用这个分组。',
|
||||
deeperReady_one: '已有 {{count}} 个同组账号启用更深连续性。这是当前最强的交接设置。',
|
||||
deeperReady_other: '已有 {{count}} 个同组账号启用更深连续性。这是当前最强的交接设置。',
|
||||
deeperNeedsPeers:
|
||||
'此账号已启用更深连续性,但同组中的其他账号也需要启用更深连续性,才能更好满足跨账号续接的预期。',
|
||||
standardWarning:
|
||||
'标准共享更适合项目上下文。如果用户期望跨账号续接,两个账号都应启用更深连续性。',
|
||||
resumeOriginalWarning: '如果旧会话很重要,请先在原账号中恢复它,再调整设置。',
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
saving: '保存中...',
|
||||
@@ -1633,9 +1752,77 @@ const resources = {
|
||||
modeSwitchDesc: '同步对话框可让用户在隔离/共享间切换并选择更深连续性。',
|
||||
historySyncGroup: '历史同步分组',
|
||||
historySyncGroupDesc: '同一分组即共享项目上下文。默认回退为 default。',
|
||||
sameGroupRule: '只有两个账号都设为共享且使用同一分组时,才会共享连续性。',
|
||||
deeperRecommendation:
|
||||
'分组“{{group}}”已开启共享,但如果用户期望跨账号续接,更推荐同时启用更深连续性。',
|
||||
partialGroup:
|
||||
'分组“{{group}}”已经配置得不错,但其他账号或分组仍需要补齐同组或更深连续性设置。',
|
||||
readyGroup: '分组“{{group}}”已有多个账号启用更深连续性。这是当前最强的交接设置。',
|
||||
legacyConfirmation_one: '{{count}} 个旧账号仍需要显式确认。',
|
||||
legacyConfirmation_other: '{{count}} 个旧账号仍需要显式确认。',
|
||||
},
|
||||
continuityReadiness: {
|
||||
title: '跨账号续接检查',
|
||||
description: '在切换账号前,用这张卡快速确认还缺什么。',
|
||||
state: {
|
||||
single: '单账号',
|
||||
isolated: '续接关闭',
|
||||
'shared-alone': '已共享但未完成',
|
||||
'shared-standard': '仅项目共享',
|
||||
partial: '部分就绪',
|
||||
ready: '强交接已就绪',
|
||||
},
|
||||
metrics: {
|
||||
isolated: '隔离账号',
|
||||
sharedPeers: '有同组伙伴',
|
||||
deeperReady: '更深已就绪',
|
||||
},
|
||||
messages: {
|
||||
single: {
|
||||
title: '目前只配置了一个 auth 账号。',
|
||||
description: '现在还不涉及跨账号交接。请先再添加一个 auth 账号,再配置共享连续性。',
|
||||
},
|
||||
isolated: {
|
||||
title: '当前还没有开启跨账号续接。',
|
||||
description: '当前可见账号都处于隔离模式,因此在一个账号中创建的会话仍会留在原账号。',
|
||||
},
|
||||
'shared-alone': {
|
||||
title: '已经开启共享,但还没有任何分组拥有同组伙伴。',
|
||||
description_one: '{{count}} 个共享账号仍在等待另一个账号加入同一分组。',
|
||||
description_other: '{{count}} 个共享账号仍在等待另一个账号加入同一分组。',
|
||||
},
|
||||
'shared-standard': {
|
||||
title: '分组“{{group}}”已共享项目上下文,但尚未形成更深连续性的配对。',
|
||||
description:
|
||||
'用户可能期望的不只是项目共享。如果希望更稳妥地支持跨账号续接,请让这个分组中的两个账号都启用更深连续性。',
|
||||
},
|
||||
partial: {
|
||||
title: '已有一个分组准备就绪,但整体配置仍是混合状态。',
|
||||
description:
|
||||
'至少有一个分组已经启用更深连续性,但其他账号仍可能处于隔离、单独共享分组,或尚未启用更深连续性。',
|
||||
},
|
||||
ready: {
|
||||
title: '分组“{{group}}”已经具备当前最强的连续性设置。',
|
||||
description:
|
||||
'这个分组中已有多个账号启用更深连续性。是否能够恢复仍取决于 Claude 上游实际保存了什么。',
|
||||
},
|
||||
},
|
||||
stepsTitle: '下一步建议',
|
||||
steps: {
|
||||
syncBoth: '请在两个账号上都打开“同步”,而不是只调整要切换进去的那个账号。',
|
||||
sameGroup: '两个账号都使用同一个历史同步分组。“{{group}}”是当前推荐的分组。',
|
||||
enableDeeper:
|
||||
'如果用户期望的是跨账号续接,而不是仅共享项目上下文,请在两个账号上都启用更深连续性。',
|
||||
resumeOriginal: '如果原来的会话很重要,请先在原账号中恢复它,再修改连续性设置。',
|
||||
},
|
||||
singleSteps: {
|
||||
addAccount: '先再创建一个 ccs auth 账号,然后再考虑跨账号交接。',
|
||||
sameGroupLater: '当第二个账号准备好后,再把两个账号放到同一个历史同步分组。',
|
||||
enableDeeperLater:
|
||||
'如果未来会期望跨账号续接而不是仅共享项目上下文,请在两个账号上都启用更深连续性。',
|
||||
resumeOriginal: '在第二个账号配置完成之前,重要会话仍应从原账号继续恢复。',
|
||||
},
|
||||
},
|
||||
accountsTable: {
|
||||
name: '名称',
|
||||
type: '类型',
|
||||
@@ -1664,6 +1851,11 @@ const resources = {
|
||||
sharedGroupLegacy: '共享({{group}},标准旧策略)',
|
||||
isolatedLegacy: '隔离(旧默认)',
|
||||
isolated: '隔离',
|
||||
noSameGroupPeer: '同组中还没有其他账号',
|
||||
sameGroupPeerCount_one: '{{count}} 个同组账号',
|
||||
sameGroupPeerCount_other: '{{count}} 个同组账号',
|
||||
legacyReview: '请检查并确认这个推断出的默认状态。',
|
||||
noHandoff: '跨账号续接仍会回到原账号。',
|
||||
},
|
||||
addAccountDialog: {
|
||||
title: '添加 {{displayName}} 账号',
|
||||
@@ -2079,6 +2271,8 @@ const resources = {
|
||||
sharedStandard: '共享标准',
|
||||
sharedStandardDesc: '仅同步项目工作区。适合大多数团队。',
|
||||
sharedDeeper: '共享更深',
|
||||
sharedDeeperDesc:
|
||||
'会额外同步 session-env、file-history、shell-snapshots 和 todos。适合需要跨账号续接的场景。',
|
||||
sharedDeeperPrefix: '额外包含',
|
||||
isolated: '隔离',
|
||||
isolatedDesc: '不建立链接。适合严格隔离场景。',
|
||||
@@ -2803,6 +2997,24 @@ const resources = {
|
||||
standardHint: 'Chế độ tiêu chuẩn chỉ đồng bộ hóa bối cảnh không gian làm việc của dự án.',
|
||||
credentialsIsolated:
|
||||
'Thông tin xác thực và .anthropic vẫn được tách riêng cho mỗi tài khoản ở tất cả các chế độ.',
|
||||
implicationTitle: 'Sau khi lưu sẽ như thế nào',
|
||||
isolatedImplication:
|
||||
'Tài khoản này sẽ tiếp tục tách biệt. Tài khoản khác sẽ không thể resume trạng thái continuity của nó.',
|
||||
sameGroupRule:
|
||||
'Các tài khoản phải dùng cùng nhóm "{{group}}" thì mới chia sẻ continuity cho nhau.',
|
||||
noSameGroupPeer: 'Hiện chưa có tài khoản nào khác dùng nhóm này.',
|
||||
sameGroupPeerCount_one: '{{count}} tài khoản khác đã dùng nhóm này.',
|
||||
sameGroupPeerCount_other: '{{count}} tài khoản khác đã dùng nhóm này.',
|
||||
deeperReady_one:
|
||||
'{{count}} tài khoản cùng nhóm đã bật deeper continuity. Đây là thiết lập handoff mạnh nhất hiện có.',
|
||||
deeperReady_other:
|
||||
'{{count}} tài khoản cùng nhóm đã bật deeper continuity. Đây là thiết lập handoff mạnh nhất hiện có.',
|
||||
deeperNeedsPeers:
|
||||
'Tài khoản này đã bật deeper continuity, nhưng tài khoản khác trong cùng nhóm vẫn cần bật deeper continuity để hỗ trợ resume xuyên tài khoản tốt hơn.',
|
||||
standardWarning:
|
||||
'Chế độ shared tiêu chuẩn chỉ phù hợp cho ngữ cảnh project. Nếu người dùng mong resume xuyên tài khoản, cả hai tài khoản nên bật deeper continuity.',
|
||||
resumeOriginalWarning:
|
||||
'Nếu cuộc trò chuyện cũ quan trọng, hãy resume nó từ tài khoản gốc trước khi đổi cấu hình.',
|
||||
cancel: 'Hủy bỏ',
|
||||
save: 'Lưu',
|
||||
saving: 'Đang lưu...',
|
||||
@@ -2826,9 +3038,89 @@ const resources = {
|
||||
historySyncGroup: 'Nhóm đồng bộ hóa lịch sử',
|
||||
historySyncGroupDesc:
|
||||
'Cùng một nhóm nghĩa là chia sẻ cùng bối cảnh project. Nếu để trống sẽ dùng nhóm mặc định.',
|
||||
sameGroupRule:
|
||||
'Hiện tại, các tài khoản chỉ chia sẻ continuity khi cả hai đều ở chế độ shared và dùng cùng một nhóm.',
|
||||
deeperRecommendation:
|
||||
'Nhóm "{{group}}" đã được chia sẻ, nhưng deeper continuity vẫn là lựa chọn an toàn hơn nếu người dùng mong resume xuyên tài khoản.',
|
||||
partialGroup:
|
||||
'Nhóm "{{group}}" đã ổn, nhưng các tài khoản hoặc nhóm khác vẫn cần hoàn thiện cấu hình cùng nhóm hoặc deeper continuity.',
|
||||
readyGroup:
|
||||
'Nhóm "{{group}}" đã có nhiều tài khoản bật deeper continuity. Đây là thiết lập handoff mạnh nhất hiện có.',
|
||||
legacyConfirmation_one: '{{count}} tài khoản cũ vẫn cần xác nhận rõ ràng.',
|
||||
legacyConfirmation_other: '{{count}} tài khoản cũ vẫn cần xác nhận rõ ràng.',
|
||||
},
|
||||
continuityReadiness: {
|
||||
title: 'Kiểm tra resume xuyên tài khoản',
|
||||
description: 'Dùng thẻ này để biết còn thiếu gì trước khi người dùng chuyển tài khoản.',
|
||||
state: {
|
||||
single: 'một tài khoản',
|
||||
isolated: 'resume tắt',
|
||||
'shared-alone': 'đã shared nhưng chưa đủ',
|
||||
'shared-standard': 'chỉ project',
|
||||
partial: 'mới sẵn sàng một phần',
|
||||
ready: 'handoff mạnh đã sẵn sàng',
|
||||
},
|
||||
metrics: {
|
||||
isolated: 'Tách biệt',
|
||||
sharedPeers: 'Có bạn cùng nhóm',
|
||||
deeperReady: 'Deeper sẵn sàng',
|
||||
},
|
||||
messages: {
|
||||
single: {
|
||||
title: 'Hiện chỉ có một auth account.',
|
||||
description:
|
||||
'Cross-account handoff chưa áp dụng. Hãy thêm một auth account khác trước khi cấu hình shared continuity.',
|
||||
},
|
||||
isolated: {
|
||||
title: 'Hiện tại cross-account resume đang tắt.',
|
||||
description:
|
||||
'Tất cả tài khoản đang hiển thị đều ở chế độ isolated, nên phiên được tạo trong một tài khoản sẽ ở lại tài khoản đó.',
|
||||
},
|
||||
'shared-alone': {
|
||||
title: 'Đã có shared mode, nhưng chưa có nhóm nào có bạn cùng nhóm.',
|
||||
description_one:
|
||||
'{{count}} shared account vẫn đang chờ một tài khoản khác vào cùng nhóm.',
|
||||
description_other:
|
||||
'{{count}} shared accounts vẫn đang chờ một tài khoản khác vào cùng nhóm.',
|
||||
},
|
||||
'shared-standard': {
|
||||
title:
|
||||
'Nhóm "{{group}}" đang chia sẻ ngữ cảnh project nhưng chưa ghép deeper continuity.',
|
||||
description:
|
||||
'Người dùng có thể mong nhiều hơn việc chia sẻ project. Hãy bật deeper continuity trên cả hai tài khoản trong nhóm này để có handoff mạnh hơn.',
|
||||
},
|
||||
partial: {
|
||||
title: 'Một nhóm đã sẵn sàng, nhưng cấu hình tổng thể vẫn còn lẫn lộn.',
|
||||
description:
|
||||
'Ít nhất một nhóm đã dùng deeper continuity, nhưng các tài khoản khác vẫn còn isolated, ở một mình trong nhóm shared, hoặc chưa bật deeper continuity.',
|
||||
},
|
||||
ready: {
|
||||
title: 'Nhóm "{{group}}" đã có thiết lập continuity mạnh nhất hiện có.',
|
||||
description:
|
||||
'Nhiều tài khoản trong nhóm này đã bật deeper continuity. Việc resume thực tế vẫn phụ thuộc vào dữ liệu mà Claude lưu ở upstream.',
|
||||
},
|
||||
},
|
||||
stepsTitle: 'Người dùng nên làm gì tiếp theo',
|
||||
steps: {
|
||||
syncBoth: 'Mở Sync trên cả hai tài khoản, không chỉ tài khoản bạn sắp chuyển sang.',
|
||||
sameGroup:
|
||||
'Dùng cùng một History Sync Group trên cả hai tài khoản. "{{group}}" là nhóm được đề xuất hiện tại.',
|
||||
enableDeeper:
|
||||
'Nếu người dùng mong resume xuyên tài khoản thay vì chỉ chia sẻ project, hãy bật deeper continuity trên cả hai tài khoản.',
|
||||
resumeOriginal:
|
||||
'Nếu cuộc trò chuyện gốc quan trọng, hãy resume nó từ tài khoản gốc trước khi đổi cài đặt continuity.',
|
||||
},
|
||||
singleSteps: {
|
||||
addAccount:
|
||||
'Hãy tạo thêm một ccs auth account thứ hai trước khi lên kế hoạch handoff xuyên tài khoản.',
|
||||
sameGroupLater:
|
||||
'Khi tài khoản thứ hai đã sẵn sàng, đặt cả hai vào cùng một History Sync Group.',
|
||||
enableDeeperLater:
|
||||
'Nếu sau này người dùng sẽ mong resume xuyên tài khoản, hãy bật deeper continuity trên cả hai tài khoản.',
|
||||
resumeOriginal:
|
||||
'Cho đến khi tài khoản thứ hai sẵn sàng, các cuộc trò chuyện quan trọng vẫn nên được resume từ tài khoản gốc.',
|
||||
},
|
||||
},
|
||||
accountsTable: {
|
||||
name: 'Tên',
|
||||
type: 'Kiểu',
|
||||
@@ -2858,6 +3150,11 @@ const resources = {
|
||||
sharedGroupLegacy: 'chia sẻ ({{group}}, kế thừa tiêu chuẩn)',
|
||||
isolatedLegacy: 'tách biệt (mặc định cũ)',
|
||||
isolated: 'tách biệt',
|
||||
noSameGroupPeer: 'Chưa có tài khoản cùng nhóm',
|
||||
sameGroupPeerCount_one: '{{count}} tài khoản cùng nhóm',
|
||||
sameGroupPeerCount_other: '{{count}} tài khoản cùng nhóm',
|
||||
legacyReview: 'Hãy xem lại và xác nhận trạng thái suy ra này.',
|
||||
noHandoff: 'Resume xuyên tài khoản vẫn sẽ quay về tài khoản gốc.',
|
||||
},
|
||||
addAccountDialog: {
|
||||
title: 'Thêm tài khoản {{displayName}}',
|
||||
@@ -3304,6 +3601,8 @@ const resources = {
|
||||
sharedStandardDesc:
|
||||
'Chỉ đồng bộ hóa không gian làm việc của dự án. Mặc định tốt nhất cho hầu hết các đội.',
|
||||
sharedDeeper: 'Chia sẻ sâu hơn',
|
||||
sharedDeeperDesc:
|
||||
'Đồng bộ thêm session-env, file-history, shell-snapshots và todos. Nên dùng khi người dùng mong resume xuyên tài khoản.',
|
||||
sharedDeeperPrefix: 'Thêm',
|
||||
isolated: 'Tách biệt',
|
||||
isolatedDesc: 'Không có liên kết. Tốt nhất cho sự tách biệt nghiêm ngặt.',
|
||||
@@ -4034,6 +4333,24 @@ const resources = {
|
||||
standardHint: '標準モードでは、プロジェクトのワークスペースコンテキストのみ同期します。',
|
||||
credentialsIsolated:
|
||||
'どのモードでも、認証情報と .anthropic はアカウントごとに分離されます。',
|
||||
implicationTitle: '保存後の挙動',
|
||||
isolatedImplication:
|
||||
'このアカウントは分離されたままです。他のアカウントからこの継続状態を再開できません。',
|
||||
sameGroupRule:
|
||||
'アカウント同士で継続性を共有するには、両方が同じグループ「{{group}}」を使う必要があります。',
|
||||
noSameGroupPeer: '現在このグループを使っている他のアカウントはありません。',
|
||||
sameGroupPeerCount_one: 'このグループを使う他のアカウントが {{count}} 件あります。',
|
||||
sameGroupPeerCount_other: 'このグループを使う他のアカウントが {{count}} 件あります。',
|
||||
deeperReady_one:
|
||||
'同じグループの {{count}} 件のアカウントがすでに拡張継続性を使っています。これは現状で最も強い引き継ぎ設定です。',
|
||||
deeperReady_other:
|
||||
'同じグループの {{count}} 件のアカウントがすでに拡張継続性を使っています。これは現状で最も強い引き継ぎ設定です。',
|
||||
deeperNeedsPeers:
|
||||
'このアカウントでは拡張継続性が有効ですが、同じグループの他アカウントでも拡張継続性を有効にしないと、アカウント間再開の期待には十分ではありません。',
|
||||
standardWarning:
|
||||
'標準共有はプロジェクト文脈向けです。アカウント間再開を期待するなら、両方のアカウントで拡張継続性を有効にしてください。',
|
||||
resumeOriginalWarning:
|
||||
'古い会話が重要なら、設定を変える前に元のアカウントで再開してください。',
|
||||
cancel: 'キャンセル',
|
||||
save: '保存',
|
||||
saving: '保存中...',
|
||||
@@ -4056,9 +4373,90 @@ const resources = {
|
||||
historySyncGroup: '履歴同期グループ',
|
||||
historySyncGroupDesc:
|
||||
'同じグループのアカウントは、同じプロジェクトのコンテキストレーンを共有します。未設定時は default を使います。',
|
||||
sameGroupRule:
|
||||
'現在、継続性を共有できるのは、両方のアカウントが共有モードかつ同じグループを使っている場合だけです。',
|
||||
deeperRecommendation:
|
||||
'グループ「{{group}}」は共有されていますが、アカウント間再開を期待するなら拡張継続性も有効にする方が安全です。',
|
||||
partialGroup:
|
||||
'グループ「{{group}}」は良い状態ですが、他のアカウントやグループでは同グループ設定や拡張継続性がまだ不足しています。',
|
||||
readyGroup:
|
||||
'グループ「{{group}}」では複数アカウントで拡張継続性が有効です。これは現状で最も強い引き継ぎ設定です。',
|
||||
legacyConfirmation_one: '{{count}} 件のレガシーアカウントで明示的な確認がまだ必要です。',
|
||||
legacyConfirmation_other: '{{count}} 件のレガシーアカウントで明示的な確認がまだ必要です。',
|
||||
},
|
||||
continuityReadiness: {
|
||||
title: 'アカウント間再開チェック',
|
||||
description: 'アカウントを切り替える前に、何が不足しているかをこのカードで確認できます。',
|
||||
state: {
|
||||
single: '単一アカウント',
|
||||
isolated: '再開オフ',
|
||||
'shared-alone': '共有だが未完成',
|
||||
'shared-standard': 'プロジェクトのみ',
|
||||
partial: '一部のみ準備完了',
|
||||
ready: '強い引き継ぎ準備完了',
|
||||
},
|
||||
metrics: {
|
||||
isolated: '分離',
|
||||
sharedPeers: '同グループあり',
|
||||
deeperReady: '拡張準備完了',
|
||||
},
|
||||
messages: {
|
||||
single: {
|
||||
title: '現在は auth アカウントが 1 つだけです。',
|
||||
description:
|
||||
'アカウント間の引き継ぎはまだ関係ありません。共有継続性を設定する前に、もう 1 つ auth アカウントを追加してください。',
|
||||
},
|
||||
isolated: {
|
||||
title: '現在、アカウント間再開はオフです。',
|
||||
description:
|
||||
'表示中のアカウントはすべて分離モードのため、あるアカウントで作成したセッションはそのアカウントに残ります。',
|
||||
},
|
||||
'shared-alone': {
|
||||
title: '共有モードはありますが、どのグループにも相手がいません。',
|
||||
description_one:
|
||||
'{{count}} 件の共有アカウントが、同じグループに入る別アカウントを待っています。',
|
||||
description_other:
|
||||
'{{count}} 件の共有アカウントが、同じグループに入る別アカウントを待っています。',
|
||||
},
|
||||
'shared-standard': {
|
||||
title:
|
||||
'グループ「{{group}}」はプロジェクト文脈を共有していますが、拡張継続性のペアがまだありません。',
|
||||
description:
|
||||
'ユーザーはプロジェクト共有以上を期待する可能性があります。より強い引き継ぎのため、このグループ内の両アカウントで拡張継続性を有効にしてください。',
|
||||
},
|
||||
partial: {
|
||||
title: '1 つのグループは準備できていますが、全体の設定はまだ混在しています。',
|
||||
description:
|
||||
'少なくとも 1 つのグループでは拡張継続性が使われていますが、他のアカウントはまだ分離状態、共有グループ内で単独、または拡張継続性が未設定です。',
|
||||
},
|
||||
ready: {
|
||||
title: 'グループ「{{group}}」は現状で最も強い継続性設定になっています。',
|
||||
description:
|
||||
'このグループでは複数アカウントがすでに拡張継続性を使っています。実際に再開できるかどうかは、Claude が上流で何を保存しているかにも依存します。',
|
||||
},
|
||||
},
|
||||
stepsTitle: '次にユーザーがやるべきこと',
|
||||
steps: {
|
||||
syncBoth:
|
||||
'切り替え先のアカウントだけでなく、両方のアカウントで「同期」を開いてください。',
|
||||
sameGroup:
|
||||
'両方のアカウントで同じ履歴同期グループを使ってください。現在の推奨グループは「{{group}}」です。',
|
||||
enableDeeper:
|
||||
'プロジェクト共有だけでなくアカウント間再開を期待するなら、両方のアカウントで拡張継続性を有効にしてください。',
|
||||
resumeOriginal:
|
||||
'元の会話が重要なら、継続性設定を変える前に元のアカウントで再開してください。',
|
||||
},
|
||||
singleSteps: {
|
||||
addAccount:
|
||||
'アカウント間引き継ぎを考える前に、まず 2 つ目の ccs auth アカウントを作成してください。',
|
||||
sameGroupLater:
|
||||
'2 つ目のアカウントができたら、両方を同じ履歴同期グループに入れてください。',
|
||||
enableDeeperLater:
|
||||
'将来アカウント間再開を期待するなら、両方のアカウントで拡張継続性を有効にしてください。',
|
||||
resumeOriginal:
|
||||
'2 つ目のアカウントが整うまでは、重要な会話は元のアカウントから再開し続けてください。',
|
||||
},
|
||||
},
|
||||
accountsTable: {
|
||||
name: '名前',
|
||||
type: '種別',
|
||||
@@ -4088,6 +4486,11 @@ const resources = {
|
||||
sharedGroupLegacy: '共有({{group}}、標準・レガシー)',
|
||||
isolatedLegacy: '分離(旧既定値)',
|
||||
isolated: '分離',
|
||||
noSameGroupPeer: '同じグループの相手がまだいません',
|
||||
sameGroupPeerCount_one: '{{count}} 件の同グループアカウント',
|
||||
sameGroupPeerCount_other: '{{count}} 件の同グループアカウント',
|
||||
legacyReview: 'この推定された既定状態を確認して明示的に確定してください。',
|
||||
noHandoff: 'アカウント間再開は元のアカウントに残ります。',
|
||||
},
|
||||
addAccountDialog: {
|
||||
title: '{{displayName}} アカウントを追加',
|
||||
@@ -4540,6 +4943,8 @@ const resources = {
|
||||
sharedStandardDesc:
|
||||
'プロジェクトワークスペースの同期のみ。ほとんどのチームに最適な既定値です。',
|
||||
sharedDeeper: '共有拡張',
|
||||
sharedDeeperDesc:
|
||||
'session-env、file-history、shell-snapshots、todos も同期します。アカウント間再開を期待するなら推奨です。',
|
||||
sharedDeeperPrefix: '追加で',
|
||||
isolated: '分離',
|
||||
isolatedDesc: 'リンクなし。厳密な分離に最適です。',
|
||||
|
||||
+43
-14
@@ -7,6 +7,7 @@ import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AlertTriangle, ArrowRight, ChevronDown, Plus, Users, Zap } from 'lucide-react';
|
||||
import { AccountsTable } from '@/components/account/accounts-table';
|
||||
import { ContinuityReadinessCard } from '@/components/account/continuity-readiness-card';
|
||||
import { CreateAuthProfileDialog } from '@/components/account/create-auth-profile-dialog';
|
||||
import { HistorySyncLearningMap } from '@/components/account/history-sync-learning-map';
|
||||
import { CopyButton } from '@/components/ui/copy-button';
|
||||
@@ -35,13 +36,13 @@ export function AccountsPage() {
|
||||
const sharedStandardCount = data?.sharedStandardCount || 0;
|
||||
const deeperSharedCount = data?.deeperSharedCount || 0;
|
||||
const isolatedCount = data?.isolatedCount || 0;
|
||||
const sharedGroups = Array.from(
|
||||
new Set(
|
||||
authAccounts
|
||||
.filter((account) => account.context_mode === 'shared')
|
||||
.map((account) => account.context_group || 'default')
|
||||
)
|
||||
).sort((a, b) => a.localeCompare(b));
|
||||
const sharedAloneCount = data?.sharedAloneCount || 0;
|
||||
const sharedPeerAccountCount = data?.sharedPeerAccountCount || 0;
|
||||
const deeperReadyAccountCount = data?.deeperReadyAccountCount || 0;
|
||||
const sharedPeerGroups = data?.sharedPeerGroups || [];
|
||||
const deeperReadyGroups = data?.deeperReadyGroups || [];
|
||||
const sharedGroups = data?.sharedGroups || [];
|
||||
const groupSummaries = data?.groupSummaries || [];
|
||||
|
||||
const legacyTargets = authAccounts.filter(
|
||||
(account) => account.context_inferred || account.continuity_inferred
|
||||
@@ -176,11 +177,7 @@ export function AccountsPage() {
|
||||
<p className="font-semibold text-foreground">
|
||||
{t('accountsPage.sharedDeeper')}
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
{t('accountsPage.sharedDeeperPrefix')} <code>session-env</code>,{' '}
|
||||
<code>file-history</code>, <code>shell-snapshots</code>,{' '}
|
||||
<code>todos</code>.
|
||||
</p>
|
||||
<p className="mt-1">{t('accountsPage.sharedDeeperDesc')}</p>
|
||||
</div>
|
||||
<div className="rounded-md border p-2.5">
|
||||
<p className="font-semibold text-foreground">
|
||||
@@ -238,11 +235,23 @@ export function AccountsPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 p-5 space-y-4 overflow-y-auto">
|
||||
<ContinuityReadinessCard
|
||||
totalAccounts={authAccounts.length}
|
||||
isolatedCount={isolatedCount}
|
||||
sharedAloneCount={sharedAloneCount}
|
||||
sharedPeerAccountCount={sharedPeerAccountCount}
|
||||
deeperReadyAccountCount={deeperReadyAccountCount}
|
||||
sharedPeerGroups={sharedPeerGroups}
|
||||
deeperReadyGroups={deeperReadyGroups}
|
||||
/>
|
||||
|
||||
<HistorySyncLearningMap
|
||||
isolatedCount={isolatedCount}
|
||||
sharedStandardCount={sharedStandardCount}
|
||||
deeperSharedCount={deeperSharedCount}
|
||||
sharedGroups={sharedGroups}
|
||||
sharedPeerGroups={sharedPeerGroups}
|
||||
deeperReadyGroups={deeperReadyGroups}
|
||||
legacyTargetCount={legacyTargetCount}
|
||||
cliproxyCount={cliproxyCount}
|
||||
/>
|
||||
@@ -258,7 +267,11 @@ export function AccountsPage() {
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground">{t('accountsPage.loadingAccounts')}</div>
|
||||
) : (
|
||||
<AccountsTable data={authAccounts} defaultAccount={data?.default ?? null} />
|
||||
<AccountsTable
|
||||
data={authAccounts}
|
||||
defaultAccount={data?.default ?? null}
|
||||
groupSummaries={groupSummaries}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -308,10 +321,22 @@ export function AccountsPage() {
|
||||
sharedStandardCount={sharedStandardCount}
|
||||
deeperSharedCount={deeperSharedCount}
|
||||
sharedGroups={sharedGroups}
|
||||
sharedPeerGroups={sharedPeerGroups}
|
||||
deeperReadyGroups={deeperReadyGroups}
|
||||
legacyTargetCount={legacyTargetCount}
|
||||
cliproxyCount={cliproxyCount}
|
||||
/>
|
||||
|
||||
<ContinuityReadinessCard
|
||||
totalAccounts={authAccounts.length}
|
||||
isolatedCount={isolatedCount}
|
||||
sharedAloneCount={sharedAloneCount}
|
||||
sharedPeerAccountCount={sharedPeerAccountCount}
|
||||
deeperReadyAccountCount={deeperReadyAccountCount}
|
||||
sharedPeerGroups={sharedPeerGroups}
|
||||
deeperReadyGroups={deeperReadyGroups}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">{t('accountsPage.accountMatrix')}</CardTitle>
|
||||
@@ -320,7 +345,11 @@ export function AccountsPage() {
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground">{t('accountsPage.loadingAccounts')}</div>
|
||||
) : (
|
||||
<AccountsTable data={authAccounts} defaultAccount={data?.default ?? null} />
|
||||
<AccountsTable
|
||||
data={authAccounts}
|
||||
defaultAccount={data?.default ?? null}
|
||||
groupSummaries={groupSummaries}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen, userEvent, waitFor } from '@tests/setup/test-utils';
|
||||
import i18n from '@/lib/i18n';
|
||||
import { ContinuityReadinessCard } from '@/components/account/continuity-readiness-card';
|
||||
import { LanguageSwitcher } from '@/components/layout/language-switcher';
|
||||
import { HistorySyncLearningMap } from '@/components/account/history-sync-learning-map';
|
||||
import { TabNavigation } from '@/pages/settings/components/tab-navigation';
|
||||
@@ -146,12 +147,19 @@ describe('Dashboard i18n', () => {
|
||||
sharedStandardCount={2}
|
||||
deeperSharedCount={3}
|
||||
sharedGroups={['default']}
|
||||
sharedPeerGroups={['default']}
|
||||
deeperReadyGroups={['default']}
|
||||
legacyTargetCount={2}
|
||||
cliproxyCount={1}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('履歴同期の仕組み')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'グループ「default」は良い状態ですが、他のアカウントやグループでは同グループ設定や拡張継続性がまだ不足しています。'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('1 件の CLIProxy Claude Pool アカウントは、CLIProxy ページで管理します。')
|
||||
).toBeInTheDocument();
|
||||
@@ -165,6 +173,70 @@ describe('Dashboard i18n', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Japanese continuity readiness card without fallback English copy', async () => {
|
||||
await i18n.changeLanguage('ja');
|
||||
|
||||
render(
|
||||
<ContinuityReadinessCard
|
||||
totalAccounts={2}
|
||||
isolatedCount={2}
|
||||
sharedAloneCount={0}
|
||||
sharedPeerAccountCount={0}
|
||||
deeperReadyAccountCount={0}
|
||||
sharedPeerGroups={[]}
|
||||
deeperReadyGroups={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('アカウント間再開チェック')).toBeInTheDocument();
|
||||
expect(screen.getByText('現在、アカウント間再開はオフです。')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses single-account-specific next steps in the readiness card', async () => {
|
||||
await i18n.changeLanguage('en');
|
||||
|
||||
render(
|
||||
<ContinuityReadinessCard
|
||||
totalAccounts={1}
|
||||
isolatedCount={1}
|
||||
sharedAloneCount={0}
|
||||
sharedPeerAccountCount={0}
|
||||
deeperReadyAccountCount={0}
|
||||
sharedPeerGroups={[]}
|
||||
deeperReadyGroups={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Create a second ccs auth account before planning any cross-account handoff.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Open Sync on both accounts, not just the one you are switching into.')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pluralizes the shared-alone readiness copy', async () => {
|
||||
await i18n.changeLanguage('en');
|
||||
|
||||
render(
|
||||
<ContinuityReadinessCard
|
||||
totalAccounts={3}
|
||||
isolatedCount={1}
|
||||
sharedAloneCount={2}
|
||||
sharedPeerAccountCount={0}
|
||||
deeperReadyAccountCount={0}
|
||||
sharedPeerGroups={[]}
|
||||
deeperReadyGroups={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('2 shared accounts are still waiting for another account in the same group.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(SUPPORTED_LOCALES.filter((locale) => locale !== 'en'))(
|
||||
'keeps %s translation keys in parity with en and preserves placeholders',
|
||||
(locale) => {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { summarizeAuthAccountContinuity } from '@/lib/account-continuity';
|
||||
import type { Account } from '@/lib/api-client';
|
||||
|
||||
function createAccount(name: string, overrides: Partial<Account> = {}): Account {
|
||||
return {
|
||||
name,
|
||||
created: '2026-04-03T00:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('summarizeAuthAccountContinuity', () => {
|
||||
it('keeps isolated accounts out of shared readiness', () => {
|
||||
const summary = summarizeAuthAccountContinuity([
|
||||
createAccount('acc1', { context_mode: 'isolated' }),
|
||||
createAccount('acc2', { context_mode: 'isolated', context_inferred: true }),
|
||||
]);
|
||||
|
||||
expect(summary.isolatedCount).toBe(2);
|
||||
expect(summary.sharedCount).toBe(0);
|
||||
expect(summary.sharedPeerGroups).toEqual([]);
|
||||
expect(summary.deeperReadyGroups).toEqual([]);
|
||||
expect(summary.accounts.map((account) => account.sameGroupPeerCount)).toEqual([0, 0]);
|
||||
});
|
||||
|
||||
it('marks shared accounts without a peer as incomplete', () => {
|
||||
const summary = summarizeAuthAccountContinuity([
|
||||
createAccount('acc1', {
|
||||
context_mode: 'shared',
|
||||
context_group: 'default',
|
||||
continuity_mode: 'standard',
|
||||
}),
|
||||
createAccount('acc2', { context_mode: 'isolated' }),
|
||||
]);
|
||||
|
||||
expect(summary.sharedCount).toBe(1);
|
||||
expect(summary.sharedAloneCount).toBe(1);
|
||||
expect(summary.sharedPeerGroups).toEqual([]);
|
||||
expect(summary.accounts[0]?.sameGroupPeerCount).toBe(0);
|
||||
});
|
||||
|
||||
it('detects same-group peers and deeper-ready groups separately', () => {
|
||||
const summary = summarizeAuthAccountContinuity([
|
||||
createAccount('acc1', {
|
||||
context_mode: 'shared',
|
||||
context_group: 'sprint-a',
|
||||
continuity_mode: 'deeper',
|
||||
}),
|
||||
createAccount('acc2', {
|
||||
context_mode: 'shared',
|
||||
context_group: 'sprint-a',
|
||||
continuity_mode: 'deeper',
|
||||
}),
|
||||
createAccount('acc3', {
|
||||
context_mode: 'shared',
|
||||
context_group: 'sprint-b',
|
||||
continuity_mode: 'standard',
|
||||
}),
|
||||
createAccount('acc4', {
|
||||
context_mode: 'shared',
|
||||
context_group: 'sprint-b',
|
||||
continuity_mode: 'standard',
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(summary.sharedPeerGroups).toEqual(['sprint-a', 'sprint-b']);
|
||||
expect(summary.deeperReadyGroups).toEqual(['sprint-a']);
|
||||
expect(summary.deeperReadyAccountCount).toBe(2);
|
||||
expect(
|
||||
summary.accounts.find((account) => account.name === 'acc1')?.sameGroupDeeperPeerCount
|
||||
).toBe(1);
|
||||
expect(
|
||||
summary.accounts.find((account) => account.name === 'acc3')?.sameGroupDeeperPeerCount
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user