fix: clarify CLIProxy setup actions

Make account setup the primary CLIProxy CTA, keep advanced variant creation secondary, and contain setup dialogs across narrow viewports.
This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-12 19:23:59 -04:00
committed by GitHub
parent 26311f7752
commit ac333dd1d5
8 changed files with 258 additions and 72 deletions
@@ -378,13 +378,13 @@ export function AddAccountDialog({
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent <DialogContent
className="sm:max-w-md" className="grid max-h-[calc(100dvh-2rem)] w-[calc(100vw-2rem)] max-w-md grid-rows-[auto_minmax(0,1fr)] overflow-hidden p-0 sm:max-w-md"
onInteractOutside={(e) => { onInteractOutside={(e) => {
// Prevent accidental close by clicking outside during auth // Prevent accidental close by clicking outside during auth
if (showAuthUI) e.preventDefault(); if (showAuthUI) e.preventDefault();
}} }}
> >
<DialogHeader> <DialogHeader className="min-w-0 px-6 pt-6 pr-12">
<DialogTitle>{t('addAccountDialog.title', { displayName })}</DialogTitle> <DialogTitle>{t('addAccountDialog.title', { displayName })}</DialogTitle>
<DialogDescription> <DialogDescription>
{isKiro {isKiro
@@ -395,7 +395,7 @@ export function AddAccountDialog({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="min-h-0 space-y-4 overflow-y-auto px-6 py-4">
{requiresAgyResponsibilityFlow && !showAuthUI && ( {requiresAgyResponsibilityFlow && !showAuthUI && (
<AntigravityResponsibilityChecklist <AntigravityResponsibilityChecklist
value={agyRiskChecklist} value={agyRiskChecklist}
@@ -748,12 +748,17 @@ export function AddAccountDialog({
)} )}
{/* Action buttons */} {/* Action buttons */}
<div className="flex items-center justify-end gap-2 pt-2"> <div className="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:items-center sm:justify-end">
<Button variant="ghost" onClick={handleCancel}> <Button variant="ghost" onClick={handleCancel} className="w-full sm:w-auto">
{t('addAccountDialog.cancel')} {t('addAccountDialog.cancel')}
</Button> </Button>
{isKiro && !showAuthUI && ( {isKiro && !showAuthUI && (
<Button variant="outline" onClick={handleKiroImport} disabled={isPending}> <Button
variant="outline"
onClick={handleKiroImport}
disabled={isPending}
className="w-full sm:w-auto"
>
{kiroImportMutation.isPending ? ( {kiroImportMutation.isPending ? (
<> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="w-4 h-4 mr-2 animate-spin" />
@@ -770,6 +775,7 @@ export function AddAccountDialog({
{!showAuthUI && ( {!showAuthUI && (
<Button <Button
onClick={handleAuthenticate} onClick={handleAuthenticate}
className="w-full sm:w-auto"
disabled={ disabled={
isPending || isPending ||
isPowerUserModePending || isPowerUserModePending ||
@@ -142,8 +142,7 @@ export function AccountsSection({
/> />
)} )}
<User className="w-4 h-4" /> <User className="w-4 h-4" />
{/* TODO i18n: missing key for "Accounts" */} {t('providerEditor.accounts')}
Accounts
{accounts.length > 0 && ( {accounts.length > 0 && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
{accounts.length} {accounts.length}
@@ -152,7 +151,7 @@ export function AccountsSection({
</h3> </h3>
<Button variant="default" size="sm" className="h-7 text-xs gap-1" onClick={onAddAccount}> <Button variant="default" size="sm" className="h-7 text-xs gap-1" onClick={onAddAccount}>
<Plus className="w-3 h-3" /> <Plus className="w-3 h-3" />
Add {t('providerEditor.addAccount')}
</Button> </Button>
</div> </div>
+7 -5
View File
@@ -177,7 +177,7 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent <DialogContent
className="sm:max-w-lg" className="grid max-h-[calc(100dvh-2rem)] w-[calc(100vw-2rem)] max-w-lg grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden p-0 sm:max-w-lg"
onPointerDownOutside={(e) => { onPointerDownOutside={(e) => {
if (step !== 'success' && step !== 'provider') { if (step !== 'success' && step !== 'provider') {
e.preventDefault(); e.preventDefault();
@@ -189,8 +189,8 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
} }
}} }}
> >
<DialogHeader> <DialogHeader className="min-w-0 px-6 pt-6 pr-12">
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex min-w-0 items-center gap-2">
<Sparkles className="w-5 h-5 text-primary" /> <Sparkles className="w-5 h-5 text-primary" />
{i18n.t('setupWizard.title')} {i18n.t('setupWizard.title')}
</DialogTitle> </DialogTitle>
@@ -203,7 +203,7 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="min-h-0 space-y-4 overflow-y-auto px-6 py-4">
{step === 'provider' && ( {step === 'provider' && (
<ProviderStep providers={PROVIDERS} onSelect={handleProviderSelect} /> <ProviderStep providers={PROVIDERS} onSelect={handleProviderSelect} />
)} )}
@@ -254,7 +254,9 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
{step === 'success' && <SuccessStep variantName={variantName} onClose={onClose} />} {step === 'success' && <SuccessStep variantName={variantName} onClose={onClose} />}
</div> </div>
<ProgressIndicator currentProgress={currentProgress} allSteps={ALL_STEPS} /> <div className="px-6 pb-6">
<ProgressIndicator currentProgress={currentProgress} allSteps={ALL_STEPS} />
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
@@ -73,12 +73,12 @@ export function VariantStep({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{selectedAccount && ( {selectedAccount && (
<div className="flex items-start gap-2 p-2 bg-muted/50 rounded-md text-sm"> <div className="flex min-w-0 items-start gap-2 rounded-md bg-muted/50 p-2 text-sm">
<User className="w-4 h-4" /> <User className="w-4 h-4 shrink-0" />
<div className="space-y-1"> <div className="min-w-0 space-y-1">
<span> <span>
{t('setupVariant.using')}{' '} {t('setupVariant.using')}{' '}
<span className={cn(privacyMode && PRIVACY_BLUR_CLASS)}> <span className={cn('break-all', privacyMode && PRIVACY_BLUR_CLASS)}>
{selectedAccountIdentity?.email} {selectedAccountIdentity?.email}
</span> </span>
</span> </span>
@@ -167,16 +167,18 @@ export function VariantStep({
value={catalogModels.some((m) => m.id === modelName) ? modelName : ''} value={catalogModels.some((m) => m.id === modelName) ? modelName : ''}
onValueChange={handleModelSelect} onValueChange={handleModelSelect}
> >
<SelectTrigger> <SelectTrigger className="min-w-0">
<SelectValue placeholder={t('setupVariant.selectModel')} /> <SelectValue placeholder={t('setupVariant.selectModel')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent className="max-w-[calc(100vw-2rem)]">
{catalogModels.map((m) => ( {catalogModels.map((m) => (
<SelectItem key={m.id} value={m.id}> <SelectItem key={m.id} value={m.id}>
<div className="flex items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<span>{m.name}</span> <span className="truncate">{m.name}</span>
{m.description && ( {m.description && (
<span className="text-xs text-muted-foreground">- {m.description}</span> <span className="truncate text-xs text-muted-foreground">
- {m.description}
</span>
)} )}
</div> </div>
</SelectItem> </SelectItem>
@@ -196,12 +198,12 @@ export function VariantStep({
</div> </div>
</div> </div>
<div className="flex items-center justify-between pt-2"> <div className="flex flex-col gap-2 pt-2 sm:flex-row sm:items-center sm:justify-between">
<Button variant="ghost" onClick={onBack}> <Button variant="ghost" onClick={onBack} className="justify-start">
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
{t('setupVariant.back')} {t('setupVariant.back')}
</Button> </Button>
<div className="flex items-center gap-2"> <div className="flex flex-col-reverse gap-2 sm:flex-row sm:items-center">
<Button variant="ghost" onClick={onSkip}> <Button variant="ghost" onClick={onSkip}>
{t('setupVariant.skip')} {t('setupVariant.skip')}
</Button> </Button>
+47 -24
View File
@@ -140,9 +140,9 @@ const resources = {
}, },
setupVariant: { setupVariant: {
using: 'Using:', using: 'Using:',
variantNameRequired: 'Variant Name *', variantNameRequired: 'Runtime Variant Name *',
variantNamePlaceholder: 'e.g., my-gemini, g3, flash', variantNamePlaceholder: 'e.g., my-gemini, g3, flash',
invokeHintPrefix: 'Use this name to invoke:', invokeHintPrefix: 'Run with this variant:',
model: 'Model', model: 'Model',
modelPlaceholder: 'e.g., gemini-2.5-pro, claude-opus-4-6-thinking', modelPlaceholder: 'e.g., gemini-2.5-pro, claude-opus-4-6-thinking',
choosePresetInstead: 'Choose from presets instead', choosePresetInstead: 'Choose from presets instead',
@@ -155,7 +155,7 @@ const resources = {
skip: 'Skip', skip: 'Skip',
creating: 'Creating...', creating: 'Creating...',
createVariant: 'Create Variant', createVariant: 'Create Variant',
skipHint: 'Skip if you just wanted to add an account without creating a variant', skipHint: 'Skip if account setup is enough and you do not need a separate runtime variant',
}, },
customPresetDialog: { customPresetDialog: {
title: 'Custom Preset', title: 'Custom Preset',
@@ -1277,6 +1277,10 @@ const resources = {
emptyControlPanelPrefix: 'For live usage stats and real-time monitoring, visit the', emptyControlPanelPrefix: 'For live usage stats and real-time monitoring, visit the',
controlPanel: 'Control Panel', controlPanel: 'Control Panel',
quickSetup: 'Quick Setup', quickSetup: 'Quick Setup',
addAccount: 'Add Account',
advancedVariant: 'Advanced Variant',
setupActionsHint:
'Add accounts here. Create variants only when you need a separate runtime/profile.',
accountManagement: 'CCS-level account management', accountManagement: 'CCS-level account management',
providers: 'Providers', providers: 'Providers',
variants: 'Variants', variants: 'Variants',
@@ -1783,11 +1787,11 @@ const resources = {
// Domain 2: Accounts / Auth // Domain 2: Accounts / Auth
// ======================================== // ========================================
setupWizard: { setupWizard: {
title: 'Quick Setup Wizard', title: 'Advanced Variant Setup',
stepProviderDesc: 'Select a provider to get started', stepProviderDesc: 'Select a provider for the runtime variant',
stepAuthDesc: 'Authenticate with your provider', stepAuthDesc: 'Authenticate with your provider',
stepAccountDesc: 'Select which account to use', stepAccountDesc: 'Select which account to use',
stepVariantDesc: 'Create your custom variant', stepVariantDesc: 'Optional: create a custom runtime variant',
stepSuccessDesc: 'Setup complete!', stepSuccessDesc: 'Setup complete!',
authStep: { authStep: {
authenticateWith: 'Authenticate with {{provider}} to add an account', authenticateWith: 'Authenticate with {{provider}} to add an account',
@@ -1948,6 +1952,8 @@ const resources = {
status: 'Status', status: 'Status',
loadingSettings: 'Loading settings...', loadingSettings: 'Loading settings...',
loadingEditor: 'Loading editor...', loadingEditor: 'Loading editor...',
accounts: 'Accounts',
addAccount: 'Add Account',
noAccountsConnected: 'No accounts connected', noAccountsConnected: 'No accounts connected',
addAccountToStart: 'Add an account to get started', addAccountToStart: 'Add an account to get started',
gcpProjectIdReadonly: 'GCP Project ID (read-only)', gcpProjectIdReadonly: 'GCP Project ID (read-only)',
@@ -2796,9 +2802,9 @@ const resources = {
}, },
setupVariant: { setupVariant: {
using: '当前使用:', using: '当前使用:',
variantNameRequired: '变体名称 *', variantNameRequired: '运行时变体名称 *',
variantNamePlaceholder: '例如:my-gemini、g3、flash', variantNamePlaceholder: '例如:my-gemini、g3、flash',
invokeHintPrefix: '可通过该名称调用', invokeHintPrefix: '使用此变体运行',
model: '模型', model: '模型',
modelPlaceholder: '例如:gemini-2.5-pro、claude-opus-4-6-thinking', modelPlaceholder: '例如:gemini-2.5-pro、claude-opus-4-6-thinking',
choosePresetInstead: '改为从预设中选择', choosePresetInstead: '改为从预设中选择',
@@ -2811,7 +2817,7 @@ const resources = {
skip: '跳过', skip: '跳过',
creating: '创建中...', creating: '创建中...',
createVariant: '创建变体', createVariant: '创建变体',
skipHint: '如果你只想添加账号而不创建变体,可以直接跳过', skipHint: '如果账号设置已足够且不需要单独运行时变体,可以跳过',
}, },
customPresetDialog: { customPresetDialog: {
title: '自定义预设', title: '自定义预设',
@@ -3855,6 +3861,9 @@ const resources = {
emptyControlPanelPrefix: '如需查看实时用量和监控,请前往', emptyControlPanelPrefix: '如需查看实时用量和监控,请前往',
controlPanel: '控制面板', controlPanel: '控制面板',
quickSetup: '快速设置', quickSetup: '快速设置',
addAccount: '添加账号',
advancedVariant: '高级变体',
setupActionsHint: '在这里添加账号。只有需要单独运行时/配置档时才创建变体。',
accountManagement: 'CCS 级账号管理', accountManagement: 'CCS 级账号管理',
providers: '提供商', providers: '提供商',
variants: '变体', variants: '变体',
@@ -4327,11 +4336,11 @@ const resources = {
failedSave: '保存失败', failedSave: '保存失败',
}, },
setupWizard: { setupWizard: {
title: '快速设置向导', title: '高级变体设置',
stepProviderDesc: '选择一个提供商开始', stepProviderDesc: '为运行时变体选择提供商',
stepAuthDesc: '完成提供商认证', stepAuthDesc: '完成提供商认证',
stepAccountDesc: '选择要使用的账号', stepAccountDesc: '选择要使用的账号',
stepVariantDesc: '创建自定义变体', stepVariantDesc: '可选:创建自定义运行时变体',
stepSuccessDesc: '设置完成!', stepSuccessDesc: '设置完成!',
authStep: { authStep: {
authenticateWith: '通过 {{provider}} 认证以添加账号', authenticateWith: '通过 {{provider}} 认证以添加账号',
@@ -4488,6 +4497,8 @@ const resources = {
status: '状态', status: '状态',
loadingSettings: '加载设置中...', loadingSettings: '加载设置中...',
loadingEditor: '加载编辑器中...', loadingEditor: '加载编辑器中...',
accounts: '账号',
addAccount: '添加账号',
noAccountsConnected: '未连接账号', noAccountsConnected: '未连接账号',
addAccountToStart: '添加账号以开始', addAccountToStart: '添加账号以开始',
gcpProjectIdReadonly: 'GCP 项目 ID(只读)', gcpProjectIdReadonly: 'GCP 项目 ID(只读)',
@@ -5314,9 +5325,9 @@ const resources = {
}, },
setupVariant: { setupVariant: {
using: 'Sử dụng:', using: 'Sử dụng:',
variantNameRequired: 'Tên biến thể *', variantNameRequired: 'Tên biến thể runtime *',
variantNamePlaceholder: 'ví dụ: my-gemini, g3, flash', variantNamePlaceholder: 'ví dụ: my-gemini, g3, flash',
invokeHintPrefix: 'Sử dụng tên này để gọi:', invokeHintPrefix: 'Chạy bằng biến thể này:',
model: 'Mô hình', model: 'Mô hình',
modelPlaceholder: 'ví dụ: gemini-2.5-pro, claude-opus-4-5', modelPlaceholder: 'ví dụ: gemini-2.5-pro, claude-opus-4-5',
choosePresetInstead: 'Hoặc chọn từ preset có sẵn', choosePresetInstead: 'Hoặc chọn từ preset có sẵn',
@@ -5329,7 +5340,7 @@ const resources = {
skip: 'Bỏ qua', skip: 'Bỏ qua',
creating: 'Đang tạo...', creating: 'Đang tạo...',
createVariant: 'Tạo biến thể', createVariant: 'Tạo biến thể',
skipHint: 'Bỏ qua nếu bạn chỉ muốn thêm tài khoản mà không tạo biến thể', skipHint: 'Bỏ qua nếu thiết lập tài khoản là đủ và không cần biến thể runtime riêng',
}, },
customPresetDialog: { customPresetDialog: {
title: 'Preset tùy chỉnh', title: 'Preset tùy chỉnh',
@@ -6460,6 +6471,9 @@ const resources = {
'Để biết số liệu thống kê sử dụng trực tiếp và theo dõi thời gian thực, hãy truy cập', 'Để biết số liệu thống kê sử dụng trực tiếp và theo dõi thời gian thực, hãy truy cập',
controlPanel: 'Bảng điều khiển', controlPanel: 'Bảng điều khiển',
quickSetup: 'Thiết lập nhanh', quickSetup: 'Thiết lập nhanh',
addAccount: 'Thêm tài khoản',
advancedVariant: 'Biến thể nâng cao',
setupActionsHint: 'Thêm tài khoản tại đây. Chỉ tạo biến thể khi cần runtime/profile riêng.',
accountManagement: 'Quản lý tài khoản cấp CCS', accountManagement: 'Quản lý tài khoản cấp CCS',
providers: 'Nhà cung cấp', providers: 'Nhà cung cấp',
variants: 'Biến thể', variants: 'Biến thể',
@@ -6940,11 +6954,11 @@ const resources = {
failedSave: 'Không lưu được', failedSave: 'Không lưu được',
}, },
setupWizard: { setupWizard: {
title: 'Trình thiết lập nhanh', title: 'Thiết lập biến thể nâng cao',
stepProviderDesc: 'Chọn nhà cung cấp để bắt đầu', stepProviderDesc: 'Chọn nhà cung cấp cho biến thể runtime',
stepAuthDesc: 'Xác thực với nhà cung cấp', stepAuthDesc: 'Xác thực với nhà cung cấp',
stepAccountDesc: 'Chọn tài khoản để sử dụng', stepAccountDesc: 'Chọn tài khoản để sử dụng',
stepVariantDesc: 'Tạo biến thể tùy chỉnh', stepVariantDesc: 'Tùy chọn: tạo biến thể runtime tùy chỉnh',
stepSuccessDesc: 'Thiết lập hoàn tất!', stepSuccessDesc: 'Thiết lập hoàn tất!',
authStep: { authStep: {
authenticateWith: 'Xác thực với {{provider}} để thêm tài khoản', authenticateWith: 'Xác thực với {{provider}} để thêm tài khoản',
@@ -7101,6 +7115,8 @@ const resources = {
status: 'Trạng thái', status: 'Trạng thái',
loadingSettings: 'Đang tải cài đặt...', loadingSettings: 'Đang tải cài đặt...',
loadingEditor: 'Đang tải trình soạn thảo...', loadingEditor: 'Đang tải trình soạn thảo...',
accounts: 'Tài khoản',
addAccount: 'Thêm tài khoản',
noAccountsConnected: 'Chưa kết nối tài khoản nào', noAccountsConnected: 'Chưa kết nối tài khoản nào',
addAccountToStart: 'Thêm tài khoản để bắt đầu', addAccountToStart: 'Thêm tài khoản để bắt đầu',
gcpProjectIdReadonly: 'GCP Project ID (chỉ đọc)', gcpProjectIdReadonly: 'GCP Project ID (chỉ đọc)',
@@ -7941,9 +7957,9 @@ const resources = {
}, },
setupVariant: { setupVariant: {
using: '使用中:', using: '使用中:',
variantNameRequired: 'バリアント名 *', variantNameRequired: 'ランタイムバリアント名 *',
variantNamePlaceholder: '例: my-gemini, g3, flash', variantNamePlaceholder: '例: my-gemini, g3, flash',
invokeHintPrefix: 'この名前で呼び出せます:', invokeHintPrefix: 'このバリアントで実行:',
model: 'モデル', model: 'モデル',
modelPlaceholder: '例: gemini-2.5-pro, claude-opus-4-6-thinking', modelPlaceholder: '例: gemini-2.5-pro, claude-opus-4-6-thinking',
choosePresetInstead: '代わりにプリセットから選択', choosePresetInstead: '代わりにプリセットから選択',
@@ -7956,7 +7972,8 @@ const resources = {
skip: 'スキップ', skip: 'スキップ',
creating: '作成中...', creating: '作成中...',
createVariant: 'バリアントを作成', createVariant: 'バリアントを作成',
skipHint: 'アカウント追加だけが目的なら、バリアント作成はスキップできます', skipHint:
'アカウント設定だけで十分で、別ランタイムバリアントが不要な場合はスキップできます',
}, },
customPresetDialog: { customPresetDialog: {
title: 'カスタムプリセット', title: 'カスタムプリセット',
@@ -9092,6 +9109,10 @@ const resources = {
emptyControlPanelPrefix: 'ライブ利用統計とリアルタイム監視は', emptyControlPanelPrefix: 'ライブ利用統計とリアルタイム監視は',
controlPanel: 'コントロールパネルで確認できます', controlPanel: 'コントロールパネルで確認できます',
quickSetup: 'クイックセットアップ', quickSetup: 'クイックセットアップ',
addAccount: 'アカウントを追加',
advancedVariant: '高度なバリアント',
setupActionsHint:
'ここでアカウントを追加します。別ランタイム/プロファイルが必要な場合のみバリアントを作成します。',
accountManagement: 'CCSレベルのアカウント管理', accountManagement: 'CCSレベルのアカウント管理',
providers: 'プロバイダー', providers: 'プロバイダー',
variants: 'バリアント', variants: 'バリアント',
@@ -10008,6 +10029,8 @@ const resources = {
status: 'ステータス', status: 'ステータス',
loadingSettings: '設定を読み込み中...', loadingSettings: '設定を読み込み中...',
loadingEditor: 'エディターを読み込み中...', loadingEditor: 'エディターを読み込み中...',
accounts: 'アカウント',
addAccount: 'アカウントを追加',
noAccountsConnected: '接続されたアカウントなし', noAccountsConnected: '接続されたアカウントなし',
addAccountToStart: 'アカウントを追加して開始', addAccountToStart: 'アカウントを追加して開始',
gcpProjectIdReadonly: 'GCP プロジェクト ID(読み取り専用)', gcpProjectIdReadonly: 'GCP プロジェクト ID(読み取り専用)',
@@ -10276,11 +10299,11 @@ const resources = {
}, },
}, },
setupWizard: { setupWizard: {
title: 'クイックセットアップウィザード', title: '高度なバリアント設定',
stepProviderDesc: 'プロバイダーを選択して開始', stepProviderDesc: 'ランタイムバリアント用のプロバイダーを選択',
stepAuthDesc: 'プロバイダーで認証', stepAuthDesc: 'プロバイダーで認証',
stepAccountDesc: '使用するアカウントを選択', stepAccountDesc: '使用するアカウントを選択',
stepVariantDesc: 'カスタムバリアントを作成', stepVariantDesc: '任意: カスタムランタイムバリアントを作成',
stepSuccessDesc: 'セットアップ完了!', stepSuccessDesc: 'セットアップ完了!',
authStep: { authStep: {
authenticateWith: '{{provider}} で認証してアカウントを追加', authenticateWith: '{{provider}} で認証してアカウントを追加',
+85 -16
View File
@@ -10,7 +10,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Check, X, RefreshCw, Sparkles, Zap, GitBranch, Trash2 } from 'lucide-react'; import { Check, X, RefreshCw, Plus, Zap, GitBranch, Trash2 } from 'lucide-react';
import { QuickSetupWizard } from '@/components/quick-setup-wizard'; import { QuickSetupWizard } from '@/components/quick-setup-wizard';
import { AddAccountDialog } from '@/components/account/add-account-dialog'; import { AddAccountDialog } from '@/components/account/add-account-dialog';
import { AccountSafetyWarningCard } from '@/components/account/account-safety-warning-card'; import { AccountSafetyWarningCard } from '@/components/account/account-safety-warning-card';
@@ -183,7 +183,13 @@ function VariantSidebarItem({
} }
// Empty state for right panel // Empty state for right panel
function EmptyProviderState({ onSetup }: { onSetup: () => void }) { function EmptyProviderState({
onAddAccount,
onCreateVariant,
}: {
onAddAccount: () => void;
onCreateVariant: () => void;
}) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="flex-1 flex items-center justify-center bg-muted/20"> <div className="flex-1 flex items-center justify-center bg-muted/20">
@@ -200,10 +206,16 @@ function EmptyProviderState({ onSetup }: { onSetup: () => void }) {
</a> </a>
. .
</p> </p>
<Button onClick={onSetup} className="gap-2"> <div className="flex flex-col justify-center gap-2 sm:flex-row">
<Sparkles className="w-4 h-4" /> <Button onClick={onAddAccount} className="gap-2">
{t('cliproxyPage.quickSetup')} <Plus className="w-4 h-4" />
</Button> {t('cliproxyPage.addAccount')}
</Button>
<Button onClick={onCreateVariant} variant="outline" className="gap-2">
<GitBranch className="w-4 h-4" />
{t('cliproxyPage.advancedVariant')}
</Button>
</div>
</div> </div>
</div> </div>
); );
@@ -305,6 +317,38 @@ export function CliproxyPage() {
const parentAuthForVariant = selectedVariantData const parentAuthForVariant = selectedVariantData
? providers.find((p) => p.provider === selectedVariantData.provider) ? providers.find((p) => p.provider === selectedVariantData.provider)
: undefined; : undefined;
const selectedAccountTarget =
selectedVariantData && parentAuthForVariant
? {
provider: selectedVariantData.provider,
displayName: parentAuthForVariant.displayName,
accountCount: parentAuthForVariant.accounts?.length || 0,
}
: selectedStatus
? {
provider: selectedStatus.provider,
displayName: selectedStatus.displayName,
accountCount: selectedStatus.accounts?.length || 0,
}
: null;
const fallbackAccountTarget = selectedVariantData
? {
provider: selectedVariantData.provider,
displayName: getProviderDisplayName(selectedVariantData.provider),
accountCount: 0,
}
: selectedProvider && isValidProvider(selectedProvider)
? {
provider: selectedProvider,
displayName: getProviderDisplayName(selectedProvider),
accountCount: 0,
}
: {
provider: 'gemini',
displayName: getProviderDisplayName('gemini'),
accountCount: 0,
};
const accountSetupTarget = selectedAccountTarget ?? fallbackAccountTarget;
const warningProvider = (selectedVariantData?.provider || selectedStatus?.provider || '') const warningProvider = (selectedVariantData?.provider || selectedStatus?.provider || '')
.toLowerCase() .toLowerCase()
.trim(); .trim();
@@ -352,6 +396,14 @@ export function CliproxyPage() {
setSelectedProvider(null); setSelectedProvider(null);
}; };
const handleAddAccountForSelectedProvider = () => {
setAddAccountProvider({
provider: accountSetupTarget.provider,
displayName: accountSetupTarget.displayName,
isFirstAccount: accountSetupTarget.accountCount === 0,
});
};
return ( return (
<div className="flex h-full min-h-0 overflow-hidden"> <div className="flex h-full min-h-0 overflow-hidden">
{/* Left Sidebar */} {/* Left Sidebar */}
@@ -377,15 +429,29 @@ export function CliproxyPage() {
{t('cliproxyPage.accountManagement')} {t('cliproxyPage.accountManagement')}
</p> </p>
<Button <div className="space-y-2">
variant="default" <Button
size="sm" variant="default"
className="w-full gap-2" size="sm"
onClick={() => setWizardOpen(true)} className="w-full gap-2"
> onClick={handleAddAccountForSelectedProvider}
<Sparkles className="w-4 h-4" /> >
{t('cliproxyPage.quickSetup')} <Plus className="w-4 h-4" />
</Button> {t('cliproxyPage.addAccount')}
</Button>
<Button
variant="outline"
size="sm"
className="w-full gap-2"
onClick={() => setWizardOpen(true)}
>
<GitBranch className="w-4 h-4" />
{t('cliproxyPage.advancedVariant')}
</Button>
<p className="text-[11px] leading-relaxed text-muted-foreground">
{t('cliproxyPage.setupActionsHint')}
</p>
</div>
</div> </div>
{/* Providers List */} {/* Providers List */}
@@ -577,7 +643,10 @@ export function CliproxyPage() {
/> />
</> </>
) : ( ) : (
<EmptyProviderState onSetup={() => setWizardOpen(true)} /> <EmptyProviderState
onAddAccount={handleAddAccountForSelectedProvider}
onCreateVariant={() => setWizardOpen(true)}
/>
)} )}
</div> </div>
@@ -15,6 +15,7 @@ const hookState = vi.hoisted(() => ({
>; >;
} }
| undefined, | undefined,
startAuthData: {} as Record<string, unknown>,
})); }));
const applyDefaultPresetMock = vi.hoisted(() => vi.fn()); const applyDefaultPresetMock = vi.hoisted(() => vi.fn());
@@ -43,7 +44,7 @@ vi.mock('@/hooks/use-cliproxy', () => ({
useStartAuth: () => ({ useStartAuth: () => ({
isPending: false, isPending: false,
mutate: (_args: unknown, options?: { onSuccess?: (data: Record<string, unknown>) => void }) => { mutate: (_args: unknown, options?: { onSuccess?: (data: Record<string, unknown>) => void }) => {
options?.onSuccess?.({}); options?.onSuccess?.(hookState.startAuthData);
}, },
}), }),
useCancelAuth: () => ({ useCancelAuth: () => ({
@@ -72,7 +73,22 @@ vi.mock('@/components/setup/wizard/steps/account-step', () => ({
})); }));
vi.mock('@/components/setup/wizard/steps/variant-step', () => ({ vi.mock('@/components/setup/wizard/steps/variant-step', () => ({
VariantStep: () => <div>variant-step</div>, VariantStep: ({
variantName,
onVariantNameChange,
}: {
variantName: string;
onVariantNameChange: (value: string) => void;
}) => (
<label>
Runtime Variant Name
<input
aria-label="Runtime Variant Name"
value={variantName}
onChange={(event) => onVariantNameChange(event.target.value)}
/>
</label>
),
})); }));
vi.mock('@/components/setup/wizard/steps/success-step', () => ({ vi.mock('@/components/setup/wizard/steps/success-step', () => ({
@@ -84,6 +100,7 @@ import { QuickSetupWizard } from '@/components/setup/wizard';
describe('QuickSetupWizard preset catalog reuse', () => { describe('QuickSetupWizard preset catalog reuse', () => {
beforeEach(() => { beforeEach(() => {
hookState.catalogData = undefined; hookState.catalogData = undefined;
hookState.startAuthData = {};
applyDefaultPresetMock.mockReset(); applyDefaultPresetMock.mockReset();
applyDefaultPresetMock.mockResolvedValue({ success: true, presetName: 'Gemini Pro' }); applyDefaultPresetMock.mockResolvedValue({ success: true, presetName: 'Gemini Pro' });
}); });
@@ -126,4 +143,10 @@ describe('QuickSetupWizard preset catalog reuse', () => {
) )
); );
}); });
it('labels the wizard as advanced variant setup', () => {
render(<QuickSetupWizard open onClose={vi.fn()} />);
expect(screen.getByRole('dialog')).toHaveTextContent('Advanced Variant Setup');
});
}); });
+66 -4
View File
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent } from '@tests/setup/test-utils'; import { render, screen, userEvent } from '@tests/setup/test-utils';
const hookState = vi.hoisted(() => ({ const hookState = vi.hoisted(() => ({
variantsData: { variants: [] as Array<{ name: string; provider: string; target?: string }> },
authData: { authData: {
authStatus: [ authStatus: [
{ {
@@ -36,7 +37,7 @@ const hookState = vi.hoisted(() => ({
vi.mock('@/hooks/use-cliproxy', () => ({ vi.mock('@/hooks/use-cliproxy', () => ({
useCliproxy: () => ({ useCliproxy: () => ({
data: { variants: [] }, data: hookState.variantsData,
isFetching: false, isFetching: false,
}), }),
useCliproxyAuth: () => ({ useCliproxyAuth: () => ({
@@ -60,7 +61,8 @@ vi.mock('@/hooks/use-cliproxy', () => ({
})); }));
vi.mock('@/components/quick-setup-wizard', () => ({ vi.mock('@/components/quick-setup-wizard', () => ({
QuickSetupWizard: () => null, QuickSetupWizard: ({ open }: { open: boolean }) =>
open ? <div data-testid="advanced-variant-wizard" /> : null,
})); }));
vi.mock('@/components/monitoring/proxy-status-widget', () => ({ vi.mock('@/components/monitoring/proxy-status-widget', () => ({
@@ -82,9 +84,21 @@ vi.mock('@/components/cliproxy/provider-editor', () => ({
})); }));
vi.mock('@/components/account/add-account-dialog', () => ({ vi.mock('@/components/account/add-account-dialog', () => ({
AddAccountDialog: ({ open, catalog }: { open: boolean; catalog?: { defaultModel?: string } }) => AddAccountDialog: ({
open,
provider,
catalog,
}: {
open: boolean;
provider?: string;
catalog?: { defaultModel?: string };
}) =>
open ? ( open ? (
<div data-testid="add-account-dialog" data-catalog={catalog?.defaultModel ?? 'missing'} /> <div
data-testid="add-account-dialog"
data-provider={provider}
data-catalog={catalog?.defaultModel ?? 'missing'}
/>
) : null, ) : null,
})); }));
@@ -92,6 +106,7 @@ import { CliproxyPage } from '@/pages/cliproxy';
describe('CliproxyPage add-account catalog gating', () => { describe('CliproxyPage add-account catalog gating', () => {
beforeEach(() => { beforeEach(() => {
hookState.variantsData = { variants: [] };
hookState.authData = { hookState.authData = {
authStatus: [ authStatus: [
{ {
@@ -154,4 +169,51 @@ describe('CliproxyPage add-account catalog gating', () => {
'gemini-3.9-pro-preview' 'gemini-3.9-pro-preview'
); );
}); });
it('uses the sidebar setup CTA for account connection instead of variant creation', async () => {
render(<CliproxyPage />);
await userEvent.click(screen.getByRole('button', { name: /add account/i }));
expect(screen.getByTestId('add-account-dialog')).toHaveAttribute('data-provider', 'gemini');
expect(screen.queryByTestId('advanced-variant-wizard')).not.toBeInTheDocument();
});
it('adds accounts against the base provider when a runtime variant is selected', async () => {
hookState.variantsData = {
variants: [{ name: 'gemini-parallel', provider: 'gemini', target: 'claude' }],
};
render(<CliproxyPage />);
await userEvent.click(screen.getByRole('button', { name: /gemini-parallel/i }));
await userEvent.click(screen.getByRole('button', { name: /add account/i }));
expect(screen.getByTestId('add-account-dialog')).toHaveAttribute('data-provider', 'gemini');
expect(screen.queryByTestId('advanced-variant-wizard')).not.toBeInTheDocument();
});
it('surfaces account setup from the empty state before provider data is available', async () => {
hookState.authData = {
authStatus: [],
source: 'local',
};
render(<CliproxyPage />);
const addAccountButtons = screen.getAllByRole('button', { name: /add account/i });
await userEvent.click(addAccountButtons[addAccountButtons.length - 1]);
expect(screen.getByTestId('add-account-dialog')).toHaveAttribute('data-provider', 'gemini');
expect(screen.queryByTestId('advanced-variant-wizard')).not.toBeInTheDocument();
});
it('keeps advanced variant creation available as a secondary action', async () => {
render(<CliproxyPage />);
await userEvent.click(screen.getByRole('button', { name: /advanced variant/i }));
expect(screen.getByTestId('advanced-variant-wizard')).toBeInTheDocument();
expect(screen.queryByTestId('add-account-dialog')).not.toBeInTheDocument();
});
}); });