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)
This commit is contained in:
kaitranntt
2025-12-27 12:04:52 -05:00
parent add4aa55c7
commit 5f59d710a6
5 changed files with 128 additions and 9 deletions
@@ -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<void> => {
// 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;
+1 -1
View File
@@ -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/",
@@ -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({
<DialogHeader>
<DialogTitle>Add {displayName} Account</DialogTitle>
<DialogDescription>
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.'}
</DialogDescription>
</DialogHeader>
@@ -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"
/>
</div>
@@ -98,10 +120,25 @@ export function AddAccountDialog({
</div>
<div className="flex items-center justify-end gap-2 pt-2">
<Button variant="ghost" onClick={onClose} disabled={startAuthMutation.isPending}>
<Button variant="ghost" onClick={onClose} disabled={isPending}>
Cancel
</Button>
<Button onClick={handleStartAuth} disabled={startAuthMutation.isPending}>
{isKiro && (
<Button variant="outline" onClick={handleKiroImport} disabled={isPending}>
{kiroImportMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Importing...
</>
) : (
<>
<Download className="w-4 h-4 mr-2" />
Import from IDE
</>
)}
</Button>
)}
<Button onClick={handleStartAuth} disabled={isPending}>
{startAuthMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
@@ -121,6 +158,11 @@ export function AddAccountDialog({
Complete the OAuth flow in your browser...
</p>
)}
{kiroImportMutation.isPending && (
<p className="text-sm text-center text-muted-foreground">
Importing token from Kiro IDE...
</p>
)}
</div>
</DialogContent>
</Dialog>
+21
View File
@@ -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({
+6
View File
@@ -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: {