From 5f59d710a687aa23b22f470114fc763bf1412fbd Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 27 Dec 2025 12:04:52 -0500 Subject: [PATCH] feat(dashboard): add Import from Kiro IDE button Add "Import from IDE" button to Dashboard AddAccountDialog for Kiro provider: - POST /api/cliproxy/auth/kiro/import endpoint using tryKiroImport() - useKiroImport() React Query hook with cache invalidation - UI button shown alongside OAuth authenticate for Kiro only - Applies default preset when importing first account - Fix UI typecheck script (remove incompatible --build flag) --- src/web-server/routes/cliproxy-auth-routes.ts | 50 ++++++++++++++++ ui/package.json | 2 +- .../components/account/add-account-dialog.tsx | 58 ++++++++++++++++--- ui/src/hooks/use-cliproxy.ts | 21 +++++++ ui/src/lib/api-client.ts | 6 ++ 5 files changed, 128 insertions(+), 9 deletions(-) diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index 637cf2d3..a4871e38 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -24,6 +24,8 @@ import { import { getProxyTarget } from '../../cliproxy/proxy-target-resolver'; import { fetchRemoteAuthStatus } from '../../cliproxy/remote-auth-fetcher'; import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader'; +import { tryKiroImport } from '../../cliproxy/auth/kiro-import'; +import { getProviderTokenDir } from '../../cliproxy/auth/token-manager'; import type { CLIProxyProvider } from '../../cliproxy/types'; const router = Router(); @@ -350,4 +352,52 @@ router.post('/project-selection/:sessionId', (req: Request, res: Response): void } }); +/** + * POST /api/cliproxy/auth/kiro/import - Import Kiro token from Kiro IDE + * Alternative auth path when OAuth callback fails to redirect properly + */ +router.post('/kiro/import', async (_req: Request, res: Response): Promise => { + // Check if remote mode is enabled - import not available remotely + const target = getProxyTarget(); + if (target.isRemote) { + res.status(501).json({ + error: 'Kiro import not available in remote mode', + }); + return; + } + + try { + const tokenDir = getProviderTokenDir('kiro'); + const result = await tryKiroImport(tokenDir, false); + + if (result.success) { + // Re-initialize accounts to pick up new token + initializeAccounts(); + + // Get the newly added account + const accounts = getProviderAccounts('kiro'); + const newAccount = accounts.find((a) => a.isDefault) || accounts[0]; + + res.json({ + success: true, + account: newAccount + ? { + id: newAccount.id, + email: newAccount.email, + provider: 'kiro', + isDefault: newAccount.isDefault, + } + : null, + }); + } else { + res.status(400).json({ + success: false, + error: result.error || 'Failed to import Kiro token', + }); + } + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + export default router; diff --git a/ui/package.json b/ui/package.json index 5fbb4629..23c50f38 100644 --- a/ui/package.json +++ b/ui/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "typecheck": "tsc -b --noEmit", + "typecheck": "tsc --noEmit", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write src/ tests/", diff --git a/ui/src/components/account/add-account-dialog.tsx b/ui/src/components/account/add-account-dialog.tsx index fa01fa4e..9d83e9c9 100644 --- a/ui/src/components/account/add-account-dialog.tsx +++ b/ui/src/components/account/add-account-dialog.tsx @@ -2,6 +2,7 @@ * Add Account Dialog Component * Triggers OAuth flow server-side to add another account to a provider * Applies default preset when adding first account + * For Kiro: Also shows "Import from IDE" option as fallback */ import { useState } from 'react'; @@ -15,8 +16,8 @@ import { import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { Loader2, ExternalLink, User } from 'lucide-react'; -import { useStartAuth } from '@/hooks/use-cliproxy'; +import { Loader2, ExternalLink, User, Download } from 'lucide-react'; +import { useStartAuth, useKiroImport } from '@/hooks/use-cliproxy'; import { applyDefaultPreset } from '@/lib/preset-utils'; import { toast } from 'sonner'; @@ -38,6 +39,10 @@ export function AddAccountDialog({ }: AddAccountDialogProps) { const [nickname, setNickname] = useState(''); const startAuthMutation = useStartAuth(); + const kiroImportMutation = useKiroImport(); + + const isKiro = provider === 'kiro'; + const isPending = startAuthMutation.isPending || kiroImportMutation.isPending; const handleStartAuth = () => { startAuthMutation.mutate( @@ -60,8 +65,24 @@ export function AddAccountDialog({ ); }; + const handleKiroImport = () => { + kiroImportMutation.mutate(undefined, { + onSuccess: async () => { + // Apply default preset if this is the first account + if (isFirstAccount) { + const result = await applyDefaultPreset('kiro'); + if (result.success && result.presetName) { + toast.success(`Applied "${result.presetName}" preset`); + } + } + setNickname(''); + onClose(); + }, + }); + }; + const handleOpenChange = (isOpen: boolean) => { - if (!isOpen && !startAuthMutation.isPending) { + if (!isOpen && !isPending) { setNickname(''); onClose(); } @@ -73,8 +94,9 @@ export function AddAccountDialog({ Add {displayName} Account - Click the button below to authenticate a new account. A browser window will open for - OAuth. + {isKiro + ? 'Authenticate via browser or import an existing token from Kiro IDE.' + : 'Click the button below to authenticate a new account. A browser window will open for OAuth.'} @@ -88,7 +110,7 @@ export function AddAccountDialog({ value={nickname} onChange={(e) => setNickname(e.target.value)} placeholder="e.g., work, personal" - disabled={startAuthMutation.isPending} + disabled={isPending} className="flex-1" /> @@ -98,10 +120,25 @@ export function AddAccountDialog({
- - + )} +
diff --git a/ui/src/hooks/use-cliproxy.ts b/ui/src/hooks/use-cliproxy.ts index fa93c06f..7dd7811b 100644 --- a/ui/src/hooks/use-cliproxy.ts +++ b/ui/src/hooks/use-cliproxy.ts @@ -136,6 +136,27 @@ export function useStartAuth() { }); } +// Kiro IDE import hook (alternative auth path when OAuth callback fails) +export function useKiroImport() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => api.cliproxy.auth.kiroImport(), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] }); + queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] }); + if (data.account) { + toast.success(`Imported Kiro account: ${data.account.email || data.account.id}`); + } else { + toast.success('Kiro token imported'); + } + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); +} + // Stats and models hooks for Overview tab export function useCliproxyStats() { return useQuery({ diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 57cdb9bd..8ececebd 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -331,6 +331,12 @@ export const api = { method: 'POST', body: JSON.stringify({ nickname }), }), + /** Import Kiro token from Kiro IDE (Kiro only) */ + kiroImport: () => + request<{ success: boolean; account: OAuthAccount | null; error?: string }>( + '/cliproxy/auth/kiro/import', + { method: 'POST' } + ), }, // Error logs errorLogs: {