From 5de6cccee08aa06d6533181a1db189a595c5e123 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Wed, 24 Dec 2025 03:15:57 -0500 Subject: [PATCH] refactor(ghcp): remove unused device code session management - Remove unused functions: registerDeviceCode, completeDeviceCode, failDeviceCode, getActiveDeviceCode, hasActiveDeviceCode, getAllActiveDeviceCodes, ActiveSession interface, activeSessions Map - Reduce device-code-handler.ts from 168 to 33 lines (YAGNI) - Add ARIA labels to copy button for accessibility - Fix timer to stop at 0 seconds instead of continuing negative - Add "(Code expired)" visual indicator with destructive styling - Change void sessionId to data-session-id attribute for debugging Code review findings applied. Gemini OAuth unaffected (uses separate project-selection-handler.ts with Authorization Code Flow). --- src/cliproxy/device-code-handler.ts | 151 +----------------- .../components/shared/device-code-dialog.tsx | 22 ++- 2 files changed, 24 insertions(+), 149 deletions(-) diff --git a/src/cliproxy/device-code-handler.ts b/src/cliproxy/device-code-handler.ts index a2dccb78..d1ccbc94 100644 --- a/src/cliproxy/device-code-handler.ts +++ b/src/cliproxy/device-code-handler.ts @@ -2,11 +2,14 @@ * Device Code Handler * * Manages device code display prompts during OAuth Device Code flow. - * Parses CLIProxyAPI stdout for device codes and broadcasts via WebSocket - * to both CLI terminal and Web UI (ccs config). + * Broadcasts device code events via WebSocket to both CLI terminal + * and Web UI (ccs config). * - * Similar pattern to project-selection-handler.ts but for device code flows - * (GitHub Copilot, Qwen, etc.) + * Events emitted by oauth-process.ts: + * - deviceCode:received - When device code is parsed from output + * - deviceCode:completed - When auth succeeds + * - deviceCode:failed - When auth fails + * - deviceCode:expired - When code expires (handled by UI timer) */ import { EventEmitter } from 'events'; @@ -22,146 +25,8 @@ export interface DeviceCodePrompt { expiresAt: number; } -/** - * Active device code session with completion resolver - */ -interface ActiveSession { - prompt: DeviceCodePrompt; - resolve: (success: boolean) => void; - timeout: NodeJS.Timeout; -} - // Global event emitter for device code events export const deviceCodeEvents = new EventEmitter(); -// Active sessions by session ID -const activeSessions = new Map(); - // Default timeout for device code (15 minutes - GitHub's default) -const DEVICE_CODE_TIMEOUT_MS = 900000; - -/** - * Parse device/user code from CLIProxy output - * - * Supports various formats: - * - "Enter code: XXXX-YYYY" - * - "code XXXX-YYYY" - * - "user code: XXXX-YYYY" - * - "code \"XXXX-YYYY\"" - */ -export function parseDeviceCode(output: string): string | null { - const codeMatch = output.match( - /(?:enter\s+)?(?:user\s+)?code[:\s]+["']?([A-Z0-9]{4,8}[-\s]?[A-Z0-9]{4,8})["']?/i - ); - return codeMatch ? codeMatch[1].toUpperCase() : null; -} - -/** - * Parse verification URL from CLIProxy output - */ -export function parseVerificationUrl(output: string): string | null { - const urlMatch = output.match(/(https?:\/\/[^\s]+device[^\s]*)/i); - return urlMatch ? urlMatch[1] : null; -} - -/** - * Register a device code session and emit event for UI broadcast - * - * @param prompt - Device code prompt data - * @returns Promise that resolves when auth completes or times out - */ -export function registerDeviceCode(prompt: DeviceCodePrompt): Promise { - return new Promise((resolve) => { - // Set timeout for session expiry - const timeout = setTimeout(() => { - const session = activeSessions.get(prompt.sessionId); - if (session) { - activeSessions.delete(prompt.sessionId); - deviceCodeEvents.emit('deviceCode:expired', prompt.sessionId); - resolve(false); - } - }, DEVICE_CODE_TIMEOUT_MS); - - // Store active session - activeSessions.set(prompt.sessionId, { - prompt, - resolve, - timeout, - }); - - // Emit event for WebSocket broadcast to UI - deviceCodeEvents.emit('deviceCode:received', prompt); - }); -} - -/** - * Mark device code session as completed (auth successful) - */ -export function completeDeviceCode(sessionId: string): boolean { - const session = activeSessions.get(sessionId); - - if (!session) { - return false; - } - - clearTimeout(session.timeout); - activeSessions.delete(sessionId); - - session.resolve(true); - deviceCodeEvents.emit('deviceCode:completed', sessionId); - - return true; -} - -/** - * Mark device code session as failed - */ -export function failDeviceCode(sessionId: string, error?: string): boolean { - const session = activeSessions.get(sessionId); - - if (!session) { - return false; - } - - clearTimeout(session.timeout); - activeSessions.delete(sessionId); - - session.resolve(false); - deviceCodeEvents.emit('deviceCode:failed', { sessionId, error }); - - return true; -} - -/** - * Get active device code prompt - */ -export function getActiveDeviceCode(sessionId: string): DeviceCodePrompt | null { - const session = activeSessions.get(sessionId); - return session ? session.prompt : null; -} - -/** - * Check if there's an active device code session - */ -export function hasActiveDeviceCode(sessionId: string): boolean { - return activeSessions.has(sessionId); -} - -/** - * Get all active device code sessions (for debugging/status) - */ -export function getAllActiveDeviceCodes(): DeviceCodePrompt[] { - return Array.from(activeSessions.values()).map((s) => s.prompt); -} - -export default { - parseDeviceCode, - parseVerificationUrl, - registerDeviceCode, - completeDeviceCode, - failDeviceCode, - getActiveDeviceCode, - hasActiveDeviceCode, - getAllActiveDeviceCodes, - deviceCodeEvents, -}; +export const DEVICE_CODE_TIMEOUT_MS = 900000; diff --git a/ui/src/components/shared/device-code-dialog.tsx b/ui/src/components/shared/device-code-dialog.tsx index a7224f3e..5f21cd50 100644 --- a/ui/src/components/shared/device-code-dialog.tsx +++ b/ui/src/components/shared/device-code-dialog.tsx @@ -52,19 +52,28 @@ export function DeviceCodeDialog({ const [hasCopied, setHasCopied] = useState(false); const [timeRemaining, setTimeRemaining] = useState(null); - // Calculate and update remaining time + // Calculate and update remaining time, stop at 0 useEffect(() => { if (!open) return; + let timer: ReturnType | null = null; + const updateTime = () => { const remaining = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000)); setTimeRemaining(remaining); + // Stop timer when expired + if (remaining === 0 && timer) { + clearInterval(timer); + timer = null; + } }; updateTime(); - const timer = setInterval(updateTime, 1000); + timer = setInterval(updateTime, 1000); - return () => clearInterval(timer); + return () => { + if (timer) clearInterval(timer); + }; }, [open, expiresAt]); const handleCopyCode = useCallback(async () => { @@ -93,12 +102,11 @@ export function DeviceCodeDialog({ return `${mins}:${secs.toString().padStart(2, '0')}`; }; - // Suppress unused variable warning - sessionId used for identification - void sessionId; + const isExpired = timeRemaining === 0; return ( !isOpen && onClose()}> - + @@ -111,6 +119,7 @@ export function DeviceCodeDialog({ (Expires in {formatTime(timeRemaining)}) )} + {isExpired && (Code expired)} @@ -127,6 +136,7 @@ export function DeviceCodeDialog({ size="icon" className="absolute top-2 right-2" onClick={handleCopyCode} + aria-label={hasCopied ? 'Code copied' : 'Copy verification code'} > {hasCopied ? (