import { useEffect, useState } from 'react'; import { type ClaudeAccountSummary, type CodexAccountSummary, type CodexFeatureSnapshot, type FastModeSnapshot, type ModelConfigSnapshot, type ModelRoleConfig, addClaudeAccount, deleteAccount, fetchAccounts, fetchCodexFeatures, fetchFastMode, fetchModelConfig, refreshAllCodexAccounts as refreshAllCodexAccountsApi, refreshCodexAccount as refreshCodexAccountApi, setCurrentCodexAccount as setCurrentCodexAccountApi, updateCodexFeatures, updateFastMode, updateModels, } from './api'; import { formatDateTime, formatJwtCacheExpiry, formatLiveStatusBadge, formatUsageBadge, } from './codexAccountBadges'; import { type Locale, type Messages } from './i18n'; import { MoaSettingsPanel } from './MoaSettingsPanel'; import { RuntimeInventorySettings } from './RuntimeInventorySettings'; import { GeneralSettings, SettingsApplyCard, SettingsNav, SettingsSectionHeading, type SettingsSectionId, } from './SettingsPanelChrome'; type AccountProvider = 'claude' | 'codex'; interface AccountData { claude: ClaudeAccountSummary[]; codex: CodexAccountSummary[]; codexCurrentIndex?: number; } export interface SettingsPanelProps { locale: Locale; nickname: string; onLocaleChange: (locale: Locale) => void; onNicknameChange: (next: string) => void; onRestartStack: () => void; t: Messages; } export function SettingsPanel({ locale, nickname, onLocaleChange, onNicknameChange, onRestartStack, t, }: SettingsPanelProps) { const [activeSection, setActiveSection] = useState('settings-general'); return (
); } function ModelSettings() { const [config, setConfig] = useState(null); const [draft, setDraft] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [savedAt, setSavedAt] = useState(null); useEffect(() => { let cancelled = false; setBusy(true); fetchModelConfig() .then((c) => { if (cancelled) return; setConfig(c); setDraft(c); setError(null); }) .catch((err) => { if (cancelled) return; setError(err instanceof Error ? err.message : String(err)); }) .finally(() => { if (!cancelled) setBusy(false); }); return () => { cancelled = true; }; }, []); async function save() { if (!draft || !config) return; setBusy(true); setError(null); try { const next = await updateModels(draft); setConfig(next); setDraft(next); setSavedAt(Date.now()); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } function setRole( role: keyof ModelConfigSnapshot, patch: Partial, ) { setDraft((prev) => prev ? { ...prev, [role]: { ...prev[role], ...patch }, } : prev, ); } const dirty = draft !== null && config !== null && JSON.stringify(draft) !== JSON.stringify(config); return (
{error ?

{error}

: null} {!draft ? (

{busy ? '불러오는 중…' : '모델 정보 없음'}

) : ( <> {(['owner', 'reviewer', 'arbiter'] as const).map((role) => (
{role} setRole(role, { model: e.target.value })} placeholder="claude / codex / claude-opus-4-7 …" type="text" value={draft[role].model} /> setRole(role, { effort: e.target.value })} placeholder="effort" type="text" value={draft[role].effort} />
))}
{savedAt && !dirty ? ( 저장됨. 적용하려면 상단의 스택 재시작을 눌러 주세요. ) : null}
)}
); } function CodexRuntimeSettings() { return (
); } function FastModeSettings() { const [state, setState] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; fetchFastMode() .then((s) => { if (cancelled) return; setState(s); setError(null); }) .catch((err) => { if (cancelled) return; setError(err instanceof Error ? err.message : String(err)); }); return () => { cancelled = true; }; }, []); async function toggle(provider: keyof FastModeSnapshot) { if (!state) return; const next = !state[provider]; const optimistic = { ...state, [provider]: next }; setState(optimistic); setBusy(true); setError(null); try { const fresh = await updateFastMode({ [provider]: next }); setState(fresh); } catch (err) { setState(state); setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } return (

패스트 모드

{error ?

{error}

: null} {!state ? (

불러오는 중…

) : ( <> )}
); } function CodexFeatureSettings() { const [state, setState] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [savedAt, setSavedAt] = useState(null); useEffect(() => { let cancelled = false; fetchCodexFeatures() .then((snapshot) => { if (cancelled) return; setState(snapshot); setError(null); }) .catch((err) => { if (cancelled) return; setError(err instanceof Error ? err.message : String(err)); }); return () => { cancelled = true; }; }, []); async function toggleGoals() { if (!state) return; const previous = state; const optimistic = { ...state, goals: !state.goals }; setState(optimistic); setBusy(true); setError(null); try { const fresh = await updateCodexFeatures({ goals: optimistic.goals }); setState(fresh); setSavedAt(Date.now()); } catch (err) { setState(previous); setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } return (

Codex 실험 기능

{error ?

{error}

: null} {!state ? (

불러오는 중…

) : ( <> {savedAt ? ( 저장됨. 적용하려면 상단의 스택 재시작을 눌러 주세요. ) : null} )}
); } function AccountSettings() { const [data, setData] = useState(null); const [busy, setBusy] = useState(false); const [perRowBusy, setPerRowBusy] = useState(null); const [error, setError] = useState(null); const [tokenInput, setTokenInput] = useState(''); async function refresh() { setBusy(true); setError(null); try { const fresh = await fetchAccounts(); setData(fresh); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } useEffect(() => { void refresh(); }, []); async function handleDelete(provider: AccountProvider, index: number) { if ( !window.confirm( `${provider} 계정 #${index} 디렉터리를 삭제합니다. 되돌릴 수 없습니다. 계속할까요?`, ) ) { return; } setBusy(true); setError(null); try { await deleteAccount(provider, index); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function handleAdd() { const token = tokenInput.trim(); if (!token) return; setBusy(true); setError(null); try { await addClaudeAccount(token); setTokenInput(''); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function handleCodexRefresh(index: number) { setPerRowBusy(`refresh:${index}`); setError(null); try { await refreshCodexAccountApi(index); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setPerRowBusy(null); } } async function handleRefreshAllCodex() { setBusy(true); setError(null); try { const result = await refreshAllCodexAccountsApi(); await refresh(); if (result.failed.length > 0) { setError( `일부 갱신 실패: ${result.failed.map((f) => `#${f.index}`).join(', ')}`, ); } } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function handleSwitchCodex(index: number) { setPerRowBusy(`switch:${index}`); setError(null); try { await setCurrentCodexAccountApi(index); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setPerRowBusy(null); } } return (
{error ?

{error}

: null} void handleAdd()} onDelete={(index) => void handleDelete('claude', index)} onTokenInputChange={setTokenInput} tokenInput={tokenInput} /> void handleDelete('codex', index)} onRefresh={(index) => void handleCodexRefresh(index)} onRefreshAll={() => void handleRefreshAllCodex()} onSwitch={(index) => void handleSwitchCodex(index)} perRowBusy={perRowBusy} />
); } function ClaudeAccounts({ busy, data, onAdd, onDelete, onTokenInputChange, tokenInput, }: { busy: boolean; data: AccountData | null; onAdd: () => void; onDelete: (index: number) => void; onTokenInputChange: (value: string) => void; tokenInput: string; }) { return (

Claude

{!data ? (

{busy ? '불러오는 중…' : '없음'}

) : data.claude.length === 0 ? (

계정 없음

) : (
    {data.claude.map((acc) => (
  • #{acc.index} {acc.subscriptionType ?? 'unknown'} {acc.rateLimitTier ? ` · ${acc.rateLimitTier}` : ''} claude 토큰 자동갱신
    {acc.index > 0 ? ( ) : ( 기본 )}
  • ))}
)}