import { useState, useRef, useCallback, useEffect } from 'react' import { X, Eye, EyeSlash, GithubLogo, SignOut, CircleNotch } from '@phosphor-icons/react' import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' import type { Settings, DeviceFlowStart, DeviceFlowPollResult, GitHubUser } from '../types' function tauriCall(cmd: string, args: Record = {}): Promise { return isTauri() ? invoke(cmd, args) : mockInvoke(cmd, args) } interface SettingsPanelProps { open: boolean settings: Settings onSave: (settings: Settings) => void onClose: () => void } interface KeyFieldProps { label: string placeholder: string value: string onChange: (value: string) => void onClear: () => void } function KeyField({ label, placeholder, value, onChange, onClear }: KeyFieldProps) { const [revealed, setRevealed] = useState(false) const inputRef = useRef(null) return (
onChange(e.target.value)} placeholder={placeholder} className="w-full border border-border bg-transparent text-foreground rounded" style={{ fontSize: 13, padding: '8px 60px 8px 10px', outline: 'none', fontFamily: 'inherit' }} autoComplete="off" data-testid={`settings-key-${label.toLowerCase().replace(/\s+/g, '-')}`} />
{value && ( <> )}
) } // --- GitHub OAuth Section --- type OAuthStatus = 'idle' | 'waiting' | 'error' interface GitHubSectionProps { githubUsername: string | null githubToken: string | null onConnected: (token: string, username: string) => void onDisconnect: () => void } /** Process a device flow poll result. Returns 'done' if auth complete, 'continue' to keep polling. */ function processPollResult( result: DeviceFlowPollResult, callbacks: { onComplete: (token: string) => Promise onExpired: () => void onError: (msg: string) => void }, ): 'done' | 'continue' { if (result.status === 'complete' && result.access_token) { callbacks.onComplete(result.access_token) return 'done' } if (result.status === 'expired') { callbacks.onExpired() return 'done' } if (result.status === 'error') { callbacks.onError(result.error ?? 'Authorization failed.') return 'done' } return 'continue' // status === 'pending' } function GitHubSection({ githubUsername, githubToken, onConnected, onDisconnect }: GitHubSectionProps) { const [oauthStatus, setOauthStatus] = useState('idle') const [userCode, setUserCode] = useState(null) const [errorMessage, setErrorMessage] = useState(null) const pollingRef = useRef(false) const deviceCodeRef = useRef(null) const isConnected = !!githubToken && !!githubUsername const stopPolling = useCallback(() => { pollingRef.current = false deviceCodeRef.current = null }, []) useEffect(() => { return () => { pollingRef.current = false } }, []) const handleLogin = useCallback(async () => { setOauthStatus('waiting') setErrorMessage(null) setUserCode(null) try { const flowStart = await tauriCall('github_device_flow_start') setUserCode(flowStart.user_code) deviceCodeRef.current = flowStart.device_code window.open(flowStart.verification_uri, '_blank') pollingRef.current = true const intervalMs = Math.max(flowStart.interval * 1000, 5000) const pollLoop = async () => { while (pollingRef.current && deviceCodeRef.current) { await new Promise(r => setTimeout(r, intervalMs)) if (!pollingRef.current) break const result = await tauriCall('github_device_flow_poll', { deviceCode: deviceCodeRef.current, }) const outcome = processPollResult(result, { onComplete: async (token) => { const user = await tauriCall('github_get_user', { token }) stopPolling() setOauthStatus('idle') setUserCode(null) onConnected(token, user.login) }, onExpired: () => { stopPolling() setOauthStatus('error') setErrorMessage('Authorization expired. Please try again.') }, onError: (msg) => { stopPolling() setOauthStatus('error') setErrorMessage(msg) }, }) if (outcome === 'done') return } } pollLoop().catch(err => { stopPolling() setOauthStatus('error') setErrorMessage(typeof err === 'string' ? err : err instanceof Error ? err.message : 'Polling failed.') }) } catch (err) { setOauthStatus('error') setErrorMessage(typeof err === 'string' ? err : err instanceof Error ? err.message : 'Failed to start login.') } }, [onConnected, stopPolling]) const resetOAuth = useCallback(() => { stopPolling() setOauthStatus('idle') setUserCode(null) setErrorMessage(null) }, [stopPolling]) if (isConnected && oauthStatus === 'idle') { return { resetOAuth(); onDisconnect() }} /> } if (oauthStatus === 'waiting' && userCode) { return } return } function GitHubConnectedRow({ username, onDisconnect }: { username: string; onDisconnect: () => void }) { return (
{username} Connected
) } function GitHubWaitingView({ userCode, onCancel }: { userCode: string; onCancel: () => void }) { return (
Enter this code on GitHub:
{userCode}
Waiting for authorization...
) } function GitHubLoginButton({ onLogin, disabled, errorMessage }: { onLogin: () => void; disabled: boolean; errorMessage: string | null }) { return (
{errorMessage && (
{errorMessage}
)}
) } // --- Settings Panel --- export function SettingsPanel({ open, settings, onSave, onClose }: SettingsPanelProps) { if (!open) return null return } function SettingsPanelInner({ settings, onSave, onClose }: Omit) { const [anthropicKey, setAnthropicKey] = useState(settings.anthropic_key ?? '') const [openaiKey, setOpenaiKey] = useState(settings.openai_key ?? '') const [googleKey, setGoogleKey] = useState(settings.google_key ?? '') const [githubToken, setGithubToken] = useState(settings.github_token) const [githubUsername, setGithubUsername] = useState(settings.github_username) const buildSettings = useCallback((ghOverride?: { token: string | null; username: string | null }): Settings => ({ anthropic_key: anthropicKey.trim() || null, openai_key: openaiKey.trim() || null, google_key: googleKey.trim() || null, github_token: ghOverride ? ghOverride.token : (githubToken ?? null), github_username: ghOverride ? ghOverride.username : (githubUsername ?? null), }), [anthropicKey, openaiKey, googleKey, githubToken, githubUsername]) const handleSave = () => { onSave(buildSettings()) onClose() } const handleGitHubConnected = useCallback((token: string, username: string) => { setGithubToken(token) setGithubUsername(username) onSave(buildSettings({ token, username })) }, [onSave, buildSettings]) const handleGitHubDisconnect = useCallback(() => { setGithubToken(null) setGithubUsername(null) onSave(buildSettings({ token: null, username: null })) }, [onSave, buildSettings]) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.stopPropagation() onClose() } if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault() handleSave() } } return (
{ if (e.target === e.currentTarget) onClose() }} onKeyDown={handleKeyDown} data-testid="settings-panel" >
) } function SettingsHeader({ onClose }: { onClose: () => void }) { return (
Settings
) } interface SettingsBodyProps { anthropicKey: string; setAnthropicKey: (v: string) => void openaiKey: string; setOpenaiKey: (v: string) => void googleKey: string; setGoogleKey: (v: string) => void githubToken: string | null; githubUsername: string | null onGitHubConnected: (token: string, username: string) => void onGitHubDisconnect: () => void } function SettingsBody(props: SettingsBodyProps) { return (
AI Provider Keys
API keys are stored locally on your device. Never sent to our servers.
props.setAnthropicKey('')} /> props.setOpenaiKey('')} /> props.setGoogleKey('')} />
GitHub
Connect your GitHub account to clone and sync vaults.
) } function SettingsFooter({ onClose, onSave }: { onClose: () => void; onSave: () => void }) { return (
{'\u2318'}, to open settings
) }