mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 00:14:15 +00:00
refactor(ui): centralize provider metadata and fallbacks
- move logo, label, and device-code copy into provider-config - handle unknown or non-string providers without UI crashes - replace duplicated hardcoded mappings in cliproxy table/logo
This commit is contained in:
1 parent
5b12ce7f00
commit
9ad8e64e85
5 files changed
+145
-85
No files matched your search
@@ -27,21 +27,12 @@ import { MoreHorizontal, Trash2, User, Pencil } from 'lucide-react';
|
|||||||
import { useDeleteVariant } from '@/hooks/use-cliproxy';
|
import { useDeleteVariant } from '@/hooks/use-cliproxy';
|
||||||
import { CliproxyEditDialog } from './cliproxy-edit-dialog';
|
import { CliproxyEditDialog } from './cliproxy-edit-dialog';
|
||||||
import type { Variant } from '@/lib/api-client';
|
import type { Variant } from '@/lib/api-client';
|
||||||
|
import { getProviderDisplayName } from '@/lib/provider-config';
|
||||||
|
|
||||||
interface CliproxyTableProps {
|
interface CliproxyTableProps {
|
||||||
data: Variant[];
|
data: Variant[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerLabels: Record<string, string> = {
|
|
||||||
gemini: 'Google Gemini',
|
|
||||||
codex: 'OpenAI Codex',
|
|
||||||
agy: 'Antigravity',
|
|
||||||
qwen: 'Alibaba Qwen',
|
|
||||||
iflow: 'iFlow',
|
|
||||||
kiro: 'Kiro (AWS)',
|
|
||||||
ghcp: 'GitHub Copilot (OAuth)',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CliproxyTable({ data }: CliproxyTableProps) {
|
export function CliproxyTable({ data }: CliproxyTableProps) {
|
||||||
const deleteMutation = useDeleteVariant();
|
const deleteMutation = useDeleteVariant();
|
||||||
const [editingVariant, setEditingVariant] = useState<Variant | null>(null);
|
const [editingVariant, setEditingVariant] = useState<Variant | null>(null);
|
||||||
@@ -59,7 +50,7 @@ export function CliproxyTable({ data }: CliproxyTableProps) {
|
|||||||
if (row.original.type === 'composite') {
|
if (row.original.type === 'composite') {
|
||||||
return <Badge variant="secondary">composite</Badge>;
|
return <Badge variant="secondary">composite</Badge>;
|
||||||
}
|
}
|
||||||
return providerLabels[row.original.provider] || row.original.provider;
|
return getProviderDisplayName(row.original.provider);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
getProviderFallbackVisual,
|
||||||
|
getProviderLogoAsset,
|
||||||
|
providerNeedsDarkLogoBackground,
|
||||||
|
} from '@/lib/provider-config';
|
||||||
|
|
||||||
interface ProviderLogoProps {
|
interface ProviderLogoProps {
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -11,35 +16,6 @@ interface ProviderLogoProps {
|
|||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provider image assets mapping */
|
|
||||||
const PROVIDER_IMAGES: Record<string, string> = {
|
|
||||||
gemini: '/assets/providers/gemini-color.svg',
|
|
||||||
codex: '/assets/providers/openai.svg',
|
|
||||||
agy: '/assets/providers/agy.png',
|
|
||||||
qwen: '/assets/providers/qwen-color.svg',
|
|
||||||
iflow: '/assets/providers/iflow.png',
|
|
||||||
kiro: '/assets/providers/kiro.png',
|
|
||||||
ghcp: '/assets/providers/copilot.svg',
|
|
||||||
claude: '/assets/providers/claude.svg',
|
|
||||||
kimi: '/assets/providers/kimi.svg',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Provider color configuration (for fallback only - no background for image logos) */
|
|
||||||
const PROVIDER_CONFIG: Record<string, { text: string; letter: string }> = {
|
|
||||||
gemini: { text: 'text-blue-600', letter: 'G' },
|
|
||||||
claude: { text: 'text-orange-600', letter: 'C' },
|
|
||||||
codex: { text: 'text-emerald-600', letter: 'X' },
|
|
||||||
agy: { text: 'text-violet-600', letter: 'A' },
|
|
||||||
qwen: { text: 'text-cyan-600', letter: 'Q' },
|
|
||||||
iflow: { text: 'text-indigo-600', letter: 'i' },
|
|
||||||
kiro: { text: 'text-teal-600', letter: 'K' },
|
|
||||||
ghcp: { text: 'text-green-600', letter: 'C' },
|
|
||||||
kimi: { text: 'text-orange-500', letter: 'K' },
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Providers whose logos require a dark background */
|
|
||||||
const DARK_BG_PROVIDERS = new Set(['kimi']);
|
|
||||||
|
|
||||||
/** Size configuration */
|
/** Size configuration */
|
||||||
const SIZE_CONFIG = {
|
const SIZE_CONFIG = {
|
||||||
sm: { container: 'w-6 h-6', icon: 'w-4 h-4', text: 'text-xs' },
|
sm: { container: 'w-6 h-6', icon: 'w-4 h-4', text: 'text-xs' },
|
||||||
@@ -48,19 +24,16 @@ const SIZE_CONFIG = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ProviderLogo({ provider, className, size = 'md' }: ProviderLogoProps) {
|
export function ProviderLogo({ provider, className, size = 'md' }: ProviderLogoProps) {
|
||||||
const providerKey = provider.toLowerCase();
|
const fallback = getProviderFallbackVisual(provider);
|
||||||
const config = PROVIDER_CONFIG[providerKey] || {
|
|
||||||
text: 'text-gray-600',
|
|
||||||
letter: provider[0]?.toUpperCase() || '?',
|
|
||||||
};
|
|
||||||
const sizeConfig = SIZE_CONFIG[size];
|
const sizeConfig = SIZE_CONFIG[size];
|
||||||
const imageSrc = PROVIDER_IMAGES[providerKey];
|
const imageSrc = getProviderLogoAsset(provider);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center rounded-md',
|
'flex items-center justify-center rounded-md',
|
||||||
imageSrc && (DARK_BG_PROVIDERS.has(providerKey) ? 'bg-gray-900 p-1' : 'bg-white p-1'),
|
imageSrc &&
|
||||||
|
(providerNeedsDarkLogoBackground(provider) ? 'bg-gray-900 p-1' : 'bg-white p-1'),
|
||||||
sizeConfig.container,
|
sizeConfig.container,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -72,7 +45,9 @@ export function ProviderLogo({ provider, className, size = 'md' }: ProviderLogoP
|
|||||||
className={cn(sizeConfig.icon, 'object-contain')}
|
className={cn(sizeConfig.icon, 'object-contain')}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className={cn('font-semibold', config.text, sizeConfig.text)}>{config.letter}</span>
|
<span className={cn('font-semibold', fallback.textClass, sizeConfig.text)}>
|
||||||
|
{fallback.letter}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import {
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ExternalLink, Copy, Check, Loader2, KeyRound } from 'lucide-react';
|
import { ExternalLink, Copy, Check, Loader2, KeyRound } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
getDeviceCodeProviderDisplayName,
|
||||||
|
getDeviceCodeProviderInstruction,
|
||||||
|
} from '@/lib/provider-config';
|
||||||
|
|
||||||
interface DeviceCodeDialogProps {
|
interface DeviceCodeDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -28,18 +32,6 @@ interface DeviceCodeDialogProps {
|
|||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provider display names */
|
|
||||||
const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
|
||||||
ghcp: 'GitHub Copilot',
|
|
||||||
qwen: 'Qwen Code',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Provider specific instructions */
|
|
||||||
const PROVIDER_INSTRUCTIONS: Record<string, string> = {
|
|
||||||
ghcp: 'Sign in with your GitHub account that has Copilot access.',
|
|
||||||
qwen: 'Sign in with your Qwen account to authorize access.',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function DeviceCodeDialog({
|
export function DeviceCodeDialog({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -91,9 +83,12 @@ export function DeviceCodeDialog({
|
|||||||
window.open(verificationUrl, '_blank', 'noopener,noreferrer');
|
window.open(verificationUrl, '_blank', 'noopener,noreferrer');
|
||||||
}, [verificationUrl]);
|
}, [verificationUrl]);
|
||||||
|
|
||||||
const providerDisplay = PROVIDER_DISPLAY_NAMES[provider] || provider;
|
const providerDisplay = getDeviceCodeProviderDisplayName(provider);
|
||||||
const instructions =
|
const instructions = getDeviceCodeProviderInstruction(provider);
|
||||||
PROVIDER_INSTRUCTIONS[provider] || 'Complete the authorization in your browser.';
|
const openActionLabel =
|
||||||
|
providerDisplay === 'Unknown provider'
|
||||||
|
? 'Open verification page'
|
||||||
|
: `Open ${providerDisplay.split(' ')[0]}`;
|
||||||
|
|
||||||
// Format remaining time
|
// Format remaining time
|
||||||
const formatTime = (seconds: number): string => {
|
const formatTime = (seconds: number): string => {
|
||||||
@@ -155,7 +150,7 @@ export function DeviceCodeDialog({
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Button onClick={handleOpenUrl} className="w-full">
|
<Button onClick={handleOpenUrl} className="w-full">
|
||||||
<ExternalLink className="w-4 h-4 mr-2" />
|
<ExternalLink className="w-4 h-4 mr-2" />
|
||||||
Open {providerDisplay.split(' ')[0]}
|
{openActionLabel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={handleCopyCode} className="w-full">
|
<Button variant="outline" onClick={handleCopyCode} className="w-full">
|
||||||
{hasCopied ? (
|
{hasCopied ? (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { getDeviceCodeProviderDisplayName } from '@/lib/provider-config';
|
||||||
|
|
||||||
export interface DeviceCodePrompt {
|
export interface DeviceCodePrompt {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -23,12 +24,13 @@ interface DeviceCodeState {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provider display names for user-friendly messages */
|
function coerceProvider(value: unknown): string {
|
||||||
const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
if (typeof value !== 'string') {
|
||||||
ghcp: 'GitHub Copilot',
|
return 'unknown';
|
||||||
kiro: 'Kiro (AWS)',
|
}
|
||||||
qwen: 'Qwen Code',
|
const normalized = value.trim().toLowerCase();
|
||||||
};
|
return normalized || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
export function useDeviceCode() {
|
export function useDeviceCode() {
|
||||||
const [state, setState] = useState<DeviceCodeState>({
|
const [state, setState] = useState<DeviceCodeState>({
|
||||||
@@ -44,14 +46,15 @@ export function useDeviceCode() {
|
|||||||
|
|
||||||
if (data.type === 'deviceCodeReceived') {
|
if (data.type === 'deviceCodeReceived') {
|
||||||
console.log('[DeviceCode] Received prompt:', data.sessionId);
|
console.log('[DeviceCode] Received prompt:', data.sessionId);
|
||||||
const displayName = PROVIDER_DISPLAY_NAMES[data.provider as string] || data.provider;
|
const provider = coerceProvider(data.provider);
|
||||||
|
const displayName = getDeviceCodeProviderDisplayName(provider);
|
||||||
toast.info(`${displayName} authorization required`);
|
toast.info(`${displayName} authorization required`);
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
prompt: {
|
prompt: {
|
||||||
sessionId: data.sessionId as string,
|
sessionId: data.sessionId as string,
|
||||||
provider: data.provider as string,
|
provider,
|
||||||
userCode: data.userCode as string,
|
userCode: data.userCode as string,
|
||||||
verificationUrl: data.verificationUrl as string,
|
verificationUrl: data.verificationUrl as string,
|
||||||
expiresAt: data.expiresAt as number,
|
expiresAt: data.expiresAt as number,
|
||||||
@@ -62,8 +65,7 @@ export function useDeviceCode() {
|
|||||||
console.log('[DeviceCode] Auth completed:', data.sessionId);
|
console.log('[DeviceCode] Auth completed:', data.sessionId);
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
if (prev.prompt && prev.prompt.sessionId === data.sessionId) {
|
if (prev.prompt && prev.prompt.sessionId === data.sessionId) {
|
||||||
const displayName =
|
const displayName = getDeviceCodeProviderDisplayName(prev.prompt.provider);
|
||||||
PROVIDER_DISPLAY_NAMES[prev.prompt.provider] || prev.prompt.provider;
|
|
||||||
toast.success(`${displayName} authentication successful!`);
|
toast.success(`${displayName} authentication successful!`);
|
||||||
return { isOpen: false, prompt: null, error: null };
|
return { isOpen: false, prompt: null, error: null };
|
||||||
}
|
}
|
||||||
@@ -73,8 +75,7 @@ export function useDeviceCode() {
|
|||||||
console.log('[DeviceCode] Auth failed:', data.sessionId, data.error);
|
console.log('[DeviceCode] Auth failed:', data.sessionId, data.error);
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
if (prev.prompt && prev.prompt.sessionId === data.sessionId) {
|
if (prev.prompt && prev.prompt.sessionId === data.sessionId) {
|
||||||
const displayName =
|
const displayName = getDeviceCodeProviderDisplayName(prev.prompt.provider);
|
||||||
PROVIDER_DISPLAY_NAMES[prev.prompt.provider] || prev.prompt.provider;
|
|
||||||
toast.error(`${displayName} authentication failed`);
|
toast.error(`${displayName} authentication failed`);
|
||||||
return { isOpen: false, prompt: null, error: data.error as string };
|
return { isOpen: false, prompt: null, error: data.error as string };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export function isValidProvider(provider: string): provider is CLIProxyProvider
|
|||||||
return CLIPROXY_PROVIDERS.includes(provider as CLIProxyProvider);
|
return CLIPROXY_PROVIDERS.includes(provider as CLIProxyProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProviderInput(provider: unknown): string {
|
||||||
|
return typeof provider === 'string' ? provider.trim().toLowerCase() : '';
|
||||||
|
}
|
||||||
|
|
||||||
interface ProviderMetadata {
|
interface ProviderMetadata {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -75,7 +79,7 @@ export const PROVIDER_METADATA: Record<CLIProxyProvider, ProviderMetadata> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Map provider names to asset filenames (only providers with actual logos)
|
// Map provider names to asset filenames (only providers with actual logos)
|
||||||
export const PROVIDER_ASSETS: Record<string, string> = {
|
export const PROVIDER_ASSETS: Record<CLIProxyProvider, string> = {
|
||||||
gemini: '/assets/providers/gemini-color.svg',
|
gemini: '/assets/providers/gemini-color.svg',
|
||||||
agy: '/assets/providers/agy.png',
|
agy: '/assets/providers/agy.png',
|
||||||
codex: '/assets/providers/openai.svg',
|
codex: '/assets/providers/openai.svg',
|
||||||
@@ -87,6 +91,56 @@ export const PROVIDER_ASSETS: Record<string, string> = {
|
|||||||
kimi: '/assets/providers/kimi.svg',
|
kimi: '/assets/providers/kimi.svg',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ProviderFallbackVisual {
|
||||||
|
textClass: string;
|
||||||
|
letter: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_PROVIDER_FALLBACK_VISUAL: ProviderFallbackVisual = {
|
||||||
|
textClass: 'text-gray-600',
|
||||||
|
letter: '?',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Fallback visual style when a provider logo asset is unavailable. */
|
||||||
|
export const PROVIDER_FALLBACK_VISUALS: Record<CLIProxyProvider, ProviderFallbackVisual> = {
|
||||||
|
gemini: { textClass: 'text-blue-600', letter: 'G' },
|
||||||
|
claude: { textClass: 'text-orange-600', letter: 'C' },
|
||||||
|
codex: { textClass: 'text-emerald-600', letter: 'X' },
|
||||||
|
agy: { textClass: 'text-violet-600', letter: 'A' },
|
||||||
|
qwen: { textClass: 'text-cyan-600', letter: 'Q' },
|
||||||
|
iflow: { textClass: 'text-indigo-600', letter: 'i' },
|
||||||
|
kiro: { textClass: 'text-teal-600', letter: 'K' },
|
||||||
|
ghcp: { textClass: 'text-green-600', letter: 'C' },
|
||||||
|
kimi: { textClass: 'text-orange-500', letter: 'K' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Providers whose logo looks better on dark background. */
|
||||||
|
export const PROVIDERS_WITH_DARK_LOGO_BG: ReadonlySet<CLIProxyProvider> = new Set(['kimi']);
|
||||||
|
|
||||||
|
export function getProviderLogoAsset(provider: unknown): string | undefined {
|
||||||
|
const normalized = normalizeProviderInput(provider);
|
||||||
|
if (!isValidProvider(normalized)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return PROVIDER_ASSETS[normalized];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProviderFallbackVisual(provider: unknown): ProviderFallbackVisual {
|
||||||
|
const normalized = normalizeProviderInput(provider);
|
||||||
|
if (isValidProvider(normalized)) {
|
||||||
|
return PROVIDER_FALLBACK_VISUALS[normalized];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...DEFAULT_PROVIDER_FALLBACK_VISUAL,
|
||||||
|
letter: normalized[0]?.toUpperCase() || DEFAULT_PROVIDER_FALLBACK_VISUAL.letter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function providerNeedsDarkLogoBackground(provider: unknown): boolean {
|
||||||
|
const normalized = normalizeProviderInput(provider);
|
||||||
|
return isValidProvider(normalized) && PROVIDERS_WITH_DARK_LOGO_BG.has(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
// Provider brand colors
|
// Provider brand colors
|
||||||
export const PROVIDER_COLORS: Record<string, string> = {
|
export const PROVIDER_COLORS: Record<string, string> = {
|
||||||
gemini: '#4285F4',
|
gemini: '#4285F4',
|
||||||
@@ -110,13 +164,17 @@ const PROVIDER_NAMES: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Map provider to display name
|
// Map provider to display name
|
||||||
export function getProviderDisplayName(provider: string): string {
|
export function getProviderDisplayName(provider: unknown): string {
|
||||||
return PROVIDER_NAMES[provider.toLowerCase()] || provider;
|
const normalized = normalizeProviderInput(provider);
|
||||||
|
if (!normalized) {
|
||||||
|
return 'Unknown provider';
|
||||||
|
}
|
||||||
|
return PROVIDER_NAMES[normalized] || String(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Map provider to user-facing short description */
|
/** Map provider to user-facing short description */
|
||||||
export function getProviderDescription(provider: string): string {
|
export function getProviderDescription(provider: unknown): string {
|
||||||
const normalized = provider.toLowerCase();
|
const normalized = normalizeProviderInput(provider);
|
||||||
if (!isValidProvider(normalized)) return '';
|
if (!isValidProvider(normalized)) return '';
|
||||||
return PROVIDER_METADATA[normalized].description;
|
return PROVIDER_METADATA[normalized].description;
|
||||||
}
|
}
|
||||||
@@ -127,17 +185,57 @@ export function getProviderDescription(provider: string): string {
|
|||||||
*/
|
*/
|
||||||
export const DEVICE_CODE_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro', 'qwen', 'kimi'];
|
export const DEVICE_CODE_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro', 'qwen', 'kimi'];
|
||||||
|
|
||||||
|
const DEVICE_CODE_PROVIDER_DISPLAY_NAMES: Readonly<Partial<Record<CLIProxyProvider, string>>> =
|
||||||
|
Object.freeze({
|
||||||
|
ghcp: 'GitHub Copilot',
|
||||||
|
kiro: 'Kiro (AWS)',
|
||||||
|
qwen: 'Qwen Code',
|
||||||
|
});
|
||||||
|
|
||||||
|
const DEVICE_CODE_PROVIDER_INSTRUCTIONS: Readonly<Partial<Record<CLIProxyProvider, string>>> =
|
||||||
|
Object.freeze({
|
||||||
|
ghcp: 'Sign in with your GitHub account that has Copilot access.',
|
||||||
|
qwen: 'Sign in with your Qwen account to authorize access.',
|
||||||
|
kiro: 'Sign in with your selected Kiro auth provider to continue.',
|
||||||
|
kimi: 'Sign in with your Kimi account and finish the device authorization.',
|
||||||
|
});
|
||||||
|
|
||||||
/** Check if provider uses Device Code flow */
|
/** Check if provider uses Device Code flow */
|
||||||
export function isDeviceCodeProvider(provider: string): boolean {
|
export function isDeviceCodeProvider(provider: unknown): boolean {
|
||||||
return DEVICE_CODE_PROVIDERS.includes(provider as CLIProxyProvider);
|
const normalized = normalizeProviderInput(provider);
|
||||||
|
return isValidProvider(normalized) && DEVICE_CODE_PROVIDERS.includes(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provider display name tuned for device-code UX copy. */
|
||||||
|
export function getDeviceCodeProviderDisplayName(provider: unknown): string {
|
||||||
|
const normalized = normalizeProviderInput(provider);
|
||||||
|
if (!normalized) {
|
||||||
|
return 'Unknown provider';
|
||||||
|
}
|
||||||
|
if (isValidProvider(normalized)) {
|
||||||
|
return DEVICE_CODE_PROVIDER_DISPLAY_NAMES[normalized] || getProviderDisplayName(normalized);
|
||||||
|
}
|
||||||
|
return String(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provider-specific helper text for device-code dialog. */
|
||||||
|
export function getDeviceCodeProviderInstruction(provider: unknown): string {
|
||||||
|
const normalized = normalizeProviderInput(provider);
|
||||||
|
if (isValidProvider(normalized)) {
|
||||||
|
return (
|
||||||
|
DEVICE_CODE_PROVIDER_INSTRUCTIONS[normalized] || 'Complete the authorization in your browser.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 'Complete the authorization in your browser.';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Providers that require nickname because token payload may not include email. */
|
/** Providers that require nickname because token payload may not include email. */
|
||||||
export const NICKNAME_REQUIRED_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro'];
|
export const NICKNAME_REQUIRED_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro'];
|
||||||
|
|
||||||
/** Check if provider requires user-supplied nickname in auth flow */
|
/** Check if provider requires user-supplied nickname in auth flow */
|
||||||
export function isNicknameRequiredProvider(provider: string): boolean {
|
export function isNicknameRequiredProvider(provider: unknown): boolean {
|
||||||
return NICKNAME_REQUIRED_PROVIDERS.includes(provider as CLIProxyProvider);
|
const normalized = normalizeProviderInput(provider);
|
||||||
|
return isValidProvider(normalized) && NICKNAME_REQUIRED_PROVIDERS.includes(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Kiro auth methods exposed in CCS UI (aligned with CLIProxyAPIPlus support). */
|
/** Kiro auth methods exposed in CCS UI (aligned with CLIProxyAPIPlus support). */
|
||||||
|
|||||||
Reference in new issue
Block a user