Skip to content

Commit 3a232c3

Browse files
committed
Merge branch 'feature/Kiro-qouta' into dev
2 parents dd85821 + 4865906 commit 3a232c3

8 files changed

Lines changed: 121 additions & 114 deletions

File tree

src-tauri/src/commands/cli_proxy.rs

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -124,47 +124,3 @@ pub async fn is_cli_proxy_running() -> CommandResult<bool> {
124124

125125
Ok(false)
126126
}
127-
128-
/// Run Kiro CLI authentication
129-
/// This spawns the CLI proxy binary with auth flags and waits for completion
130-
#[command]
131-
pub async fn run_kiro_auth(exe_path: String, auth_method: String) -> CommandResult<String> {
132-
// Map auth method to CLI flag
133-
let auth_flag = match auth_method.as_str() {
134-
"google" => "-kiro-google-login",
135-
"aws" => "-kiro-aws-login",
136-
"aws-authcode" => "-kiro-aws-authcode",
137-
"import" => "-kiro-import",
138-
_ => return Err(CommandError::General(format!("Unknown auth method: {}", auth_method))),
139-
};
140-
141-
// Get working directory from exe path
142-
let exe = std::path::PathBuf::from(&exe_path);
143-
let work_dir = exe.parent()
144-
.ok_or_else(|| CommandError::General("Invalid path".into()))?;
145-
146-
// Spawn process and wait for completion
147-
#[cfg(windows)]
148-
let output = Command::new(&exe_path)
149-
.arg(auth_flag)
150-
.current_dir(work_dir)
151-
.creation_flags(CREATE_NO_WINDOW)
152-
.output()
153-
.map_err(|e| CommandError::General(format!("Failed to start auth: {}", e)))?;
154-
155-
#[cfg(not(windows))]
156-
let output = Command::new(&exe_path)
157-
.arg(auth_flag)
158-
.current_dir(work_dir)
159-
.output()
160-
.map_err(|e| CommandError::General(format!("Failed to start auth: {}", e)))?;
161-
162-
if output.status.success() {
163-
Ok("Authentication completed successfully".to_string())
164-
} else {
165-
let stderr = String::from_utf8_lossy(&output.stderr);
166-
let stdout = String::from_utf8_lossy(&output.stdout);
167-
let message = if !stderr.is_empty() { stderr.to_string() } else { stdout.to_string() };
168-
Err(CommandError::General(format!("Auth failed: {}", message.trim())))
169-
}
170-
}

src-tauri/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ pub fn run() {
9090
start_cli_proxy,
9191
stop_cli_proxy,
9292
is_cli_proxy_running,
93-
run_kiro_auth,
9493
])
9594
.build(tauri::generate_context!())
9695
.expect("error while building tauri application")

src/components/quota/CompactQuotaCard.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import { Card } from '@/components/ui/card';
66
import { Badge } from '@/components/ui/badge';
7-
import { Clock, RefreshCw, Eye, List, ChevronDown } from 'lucide-react';
7+
import { Clock, RefreshCw, Eye, List, ChevronDown, Ban } from 'lucide-react';
88
import { useMemo, useState } from 'react';
99
import {
1010
Dialog,
@@ -40,11 +40,15 @@ export function CompactQuotaCard({
4040
loading,
4141
error,
4242
items,
43+
plan,
4344
onRefresh,
4445
isPrivacyMode
4546
}: CompactQuotaCardProps) {
4647
const [isExpanded, setIsExpanded] = useState(true);
4748

49+
// Check if account is suspended
50+
const isSuspended = plan?.toLowerCase() === 'suspended';
51+
4852
// Apply masking
4953
const displayEmail = isPrivacyMode ? maskEmail(email || '') : (email || '********@*****.com');
5054

@@ -94,7 +98,7 @@ export function CompactQuotaCard({
9498
};
9599

96100
return (
97-
<Card className="p-4 space-y-3 hover:shadow-md transition-shadow">
101+
<Card className="p-4 space-y-3 hover:shadow-md transition-shadow h-full flex flex-col">
98102
{/* Email with expand/collapse toggle */}
99103
<div className="flex items-center justify-between min-w-0">
100104
<div className="flex items-center gap-2 min-w-0 flex-1">
@@ -119,9 +123,17 @@ export function CompactQuotaCard({
119123
<div className="text-xs text-destructive truncate">{error}</div>
120124
)}
121125

126+
{/* Suspended State - centered icon and text */}
127+
{!loading && !error && isSuspended && (
128+
<div className="flex flex-col items-center justify-center py-6 gap-2 flex-1">
129+
<Ban className="h-10 w-10 text-yellow-500" />
130+
<span className="text-sm font-semibold text-yellow-600">Temporarily Suspended</span>
131+
</div>
132+
)}
133+
122134
{/* Model badges grid with progress bars - expanded view */}
123-
{!loading && !error && isExpanded && (
124-
<div className="grid grid-cols-1 gap-2">
135+
{!loading && !error && !isSuspended && isExpanded && (
136+
<div className="grid grid-cols-1 gap-2 flex-1">
125137
{groupedItems.map((group, idx) => (
126138
<div key={idx} className="space-y-1">
127139
{/* Model info row */}
@@ -150,7 +162,7 @@ export function CompactQuotaCard({
150162
)}
151163

152164
{/* Collapsed compact summary view - simple inline text */}
153-
{!loading && !error && !isExpanded && (
165+
{!loading && !error && !isSuspended && !isExpanded && (
154166
<div className="text-xs text-muted-foreground space-y-1">
155167
{groupedItems.map((group, idx) => (
156168
<div key={idx} className="flex items-center justify-between gap-2 min-w-0">
@@ -172,7 +184,7 @@ export function CompactQuotaCard({
172184
)}
173185

174186
{/* Footer Row */}
175-
<div className="flex items-center justify-between pt-2 border-t text-xs text-muted-foreground">
187+
<div className="flex items-center justify-between pt-2 border-t text-xs text-muted-foreground mt-auto">
176188
<span>{new Date().toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}, {new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })}</span>
177189
<div className="flex items-center gap-1">
178190
{/* View Details Dialog */}

src/components/quota/ProviderQuotaCard.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Card, CardContent } from '@/components/ui/card';
22
import { Badge } from '@/components/ui/badge';
3-
import { RefreshCw, User, Clock, Search, Folder, List } from 'lucide-react';
3+
import { RefreshCw, User, Clock, Search, Folder, List, Ban } from 'lucide-react';
44
import { useTranslation } from 'react-i18next';
55
import { Button } from '@/components/ui/button';
66
import {
@@ -28,6 +28,7 @@ interface ProviderQuotaCardProps {
2828
loading: boolean;
2929
error?: string;
3030
items: QuotaItem[];
31+
plan?: string;
3132
onRefresh: () => void;
3233
isPrivacyMode: boolean;
3334
}
@@ -39,6 +40,7 @@ export function ProviderQuotaCard({
3940
loading,
4041
error,
4142
items,
43+
plan,
4244
onRefresh,
4345
isPrivacyMode
4446
}: ProviderQuotaCardProps) {
@@ -102,6 +104,9 @@ export function ProviderQuotaCard({
102104
}).sort((a, b) => a.name.localeCompare(b.name));
103105
}, [items, provider]);
104106

107+
// Check if account is suspended
108+
const isSuspended = plan?.toLowerCase() === 'suspended';
109+
105110
return (
106111
<Card className="mb-4 overflow-hidden border bg-card text-card-foreground p-0">
107112
{/* Header Section */}
@@ -222,12 +227,19 @@ export function ProviderQuotaCard({
222227
{t('quotaCard.usage')}
223228
</div>
224229

225-
{groupedItems.length === 0 && !loading && (
230+
{groupedItems.length === 0 && !loading && !isSuspended && (
226231
<div className="text-sm italic text-muted-foreground">{t('quotaCard.noUsage')}</div>
227232
)}
228233

234+
{/* Suspended State */}
235+
{isSuspended && (
236+
<div className="flex flex-col items-center justify-center py-8 gap-3">
237+
<Ban className="h-12 w-12 text-yellow-500" />
238+
<span className="text-lg font-semibold text-yellow-600">Temporarily Suspended</span>
239+
</div>
240+
)}
229241

230-
{groupedItems.map((group, idx) => (
242+
{!isSuspended && groupedItems.map((group, idx) => (
231243
<div key={idx} className="space-y-2">
232244
<div className="flex items-center justify-between text-sm">
233245
<div className="flex items-center gap-2">

src/pages/ProvidersPage.tsx

Lines changed: 74 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ import {
2525
Eye,
2626
EyeOff
2727
} from 'lucide-react';
28-
import { openExternalUrl, isTauri, runKiroAuth } from '@/services/tauri';
29-
import { useCliProxyStore } from '@/stores';
28+
import { openExternalUrl, isTauri } from '@/services/tauri';
3029
import {
3130
AlertDialog,
3231
AlertDialogAction,
@@ -72,12 +71,23 @@ function formatName(name: string | undefined | null): string {
7271
return name.replace(/_gmail_com/g, '').replace(/\.json$/g, '');
7372
}
7473

74+
// Helper to get provider icon path
75+
function getProviderIconPath(providerId: string): string {
76+
const id = providerId.toLowerCase();
77+
if (id.includes('antigravity')) return '/antigravity/antigravity.png';
78+
if (id.includes('claude') || id.includes('anthropic')) return '/claude/claude.png';
79+
if (id.includes('gemini')) return '/gemini/gemini.png';
80+
if (id.includes('codex') || id.includes('openai')) return '/openai/openai.png';
81+
if (id.includes('kiro')) return '/kiro/kiro.png';
82+
return '';
83+
}
84+
7585

7686
export function ProvidersPage() {
7787
const { t } = useTranslation();
7888
const { isAuthenticated } = useAuthStore();
7989

80-
// Confimration state
90+
// Confirmation state
8191
const [fileToDelete, setFileToDelete] = useState<string | null>(null);
8292

8393
// --- State: Connected Accounts ---
@@ -182,27 +192,45 @@ export function ProvidersPage() {
182192
updateProviderState(providerId, { status: 'waiting', error: undefined });
183193
setSelectedProvider(providerId);
184194

185-
if (providerId === 'kiro') {
186-
try {
187-
const { exePath } = useCliProxyStore.getState();
188-
if (!exePath) {
189-
throw new Error('CLI Proxy path not configured. Please set it in Settings.');
190-
}
191-
await runKiroAuth(exePath, 'google');
192-
updateProviderState(providerId, { status: 'success' });
193-
setSelectedProvider(null);
194-
await new Promise(resolve => setTimeout(resolve, 500));
195-
await loadFiles();
196-
} catch (err) {
197-
updateProviderState(providerId, {
198-
status: 'error',
199-
error: (err as Error).message,
200-
});
195+
try {
196+
// Kiro uses a dedicated web OAuth page
197+
if (providerId === 'kiro') {
198+
const { apiBase } = useAuthStore.getState();
199+
const kiroOAuthUrl = `${apiBase}/v0/oauth/kiro`;
200+
201+
const initialFiles = files.filter(f =>
202+
f.provider?.toLowerCase().includes('kiro') ||
203+
f.filename?.toLowerCase().includes('kiro')
204+
);
205+
const initialKiroCount = initialFiles.length;
206+
207+
await openInBrowser(kiroOAuthUrl);
208+
updateProviderState(providerId, { url: kiroOAuthUrl, status: 'polling' });
209+
210+
const pollTimer = window.setInterval(async () => {
211+
try {
212+
const response = await authFilesApi.list();
213+
const currentFiles = response?.files ?? [];
214+
const currentKiroFiles = currentFiles.filter(f =>
215+
f.provider?.toLowerCase().includes('kiro') ||
216+
f.filename?.toLowerCase().includes('kiro')
217+
);
218+
219+
// Detect if a new kiro file was added
220+
if (currentKiroFiles.length > initialKiroCount) {
221+
updateProviderState(providerId, { status: 'success' });
222+
stopPolling(providerId);
223+
setSelectedProvider(null);
224+
setFiles(currentFiles);
225+
}
226+
} catch {
227+
// Ignore polling errors - expected during network issues
228+
}
229+
}, 2000);
230+
pollingTimers.current[providerId] = pollTimer;
231+
return;
201232
}
202-
return;
203-
}
204233

205-
try {
206234
const response = await oauthApi.startAuth(providerId, options);
207235
const url = response.url || response.auth_url;
208236
const state = response.state;
@@ -253,11 +281,11 @@ export function ProvidersPage() {
253281
}
254282
};
255283

256-
const copyToClipboard = async (text: string) => {
284+
const copyToClipboard = useCallback(async (text: string) => {
257285
try {
258286
await navigator.clipboard.writeText(text);
259287
} catch { /* ignore */ }
260-
};
288+
}, []);
261289

262290
if (!isAuthenticated) {
263291
return (
@@ -331,32 +359,34 @@ export function ProvidersPage() {
331359

332360
<div className="space-y-1">
333361
{files.map((file) => {
334-
let iconPath = '';
362+
const iconPath = getProviderIconPath(file.provider || '');
335363
const p = (file.provider || '').toLowerCase();
336-
if (p.includes('antigravity')) iconPath = '/antigravity/antigravity.png';
337-
else if (p.includes('claude') || p.includes('anthropic')) iconPath = '/claude/claude.png';
338-
else if (p.includes('gemini')) iconPath = '/gemini/gemini.png';
339-
else if (p.includes('codex') || p.includes('openai')) iconPath = '/openai/openai.png';
340-
else if (p.includes('kiro')) iconPath = '/kiro/kiro.png';
341364

342365
let rawName: string;
343366
if (p.includes('kiro')) {
344-
const filename = file.filename || file.id || '';
345-
const match = filename.match(/kiro-(\w+)/i);
346-
347-
if (match && match[1]) {
348-
const method = match[1].charAt(0).toUpperCase() + match[1].slice(1).toLowerCase();
349-
rawName = `Kiro (${method})`;
367+
// First try top-level email/account fields
368+
const topEmail = (file.email as string) || (file.account as string);
369+
if (topEmail && topEmail.trim() !== '') {
370+
rawName = formatName(topEmail);
350371
} else {
351-
const metaEmail = (file.metadata?.email as string) || (file['email'] as string);
352-
const authMethod = (file.metadata?.provider as string) || (file.metadata?.auth_method as string);
372+
// Fallback to filename parsing or metadata
373+
const filename = file.filename || file.id || '';
374+
const match = filename.match(/kiro-(\\w+)/i);
353375

354-
if (metaEmail && metaEmail.trim() !== '') {
355-
rawName = formatName(metaEmail);
356-
} else if (authMethod) {
357-
rawName = `Kiro (${authMethod})`;
376+
if (match && match[1]) {
377+
const method = match[1].charAt(0).toUpperCase() + match[1].slice(1).toLowerCase();
378+
rawName = `Kiro (${method})`;
358379
} else {
359-
rawName = 'Kiro';
380+
const metaEmail = (file.metadata?.email as string);
381+
const authMethod = (file.metadata?.provider as string) || (file.metadata?.auth_method as string);
382+
383+
if (metaEmail && metaEmail.trim() !== '') {
384+
rawName = formatName(metaEmail);
385+
} else if (authMethod) {
386+
rawName = `Kiro (${authMethod})`;
387+
} else {
388+
rawName = 'Kiro';
389+
}
360390
}
361391
}
362392
} else {
@@ -426,13 +456,7 @@ export function ProvidersPage() {
426456
const isWaiting = state.status === 'waiting' || state.status === 'polling';
427457
const isSuccess = state.status === 'success';
428458

429-
let iconPath = '';
430-
const pid = provider.id;
431-
if (pid === 'antigravity') iconPath = '/antigravity/antigravity.png';
432-
else if (pid === 'anthropic' || (pid as string) === 'claude') iconPath = '/claude/claude.png';
433-
else if (pid === 'gemini-cli') iconPath = '/gemini/gemini.png';
434-
else if (pid === 'codex') iconPath = '/openai/openai.png';
435-
else if (pid === 'kiro') iconPath = '/kiro/kiro.png';
459+
const iconPath = getProviderIconPath(provider.id);
436460

437461
const isSelected = selectedProvider === provider.id;
438462

src/pages/QuotaPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ export function QuotaPage() {
416416
loading={file.loading}
417417
error={file.error}
418418
items={items}
419+
plan={file.plan}
419420
onRefresh={() => fetchQuotaForFile(file.fileId, file.originalFile)}
420421
isPrivacyMode={isPrivacyMode}
421422
/>

0 commit comments

Comments
 (0)