diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index f150c38..5428a92 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -56,7 +56,7 @@ Codex 관련 선택 설정은 **별도 서비스 파일이 아니라 같은 `.en ```bash # 같은 .env 또는 서비스 Environment=에 추가 가능 -CODEX_MODEL=gpt-5.4 +CODEX_MODEL=gpt-5.5 CODEX_EFFORT=xhigh OPENAI_API_KEY=... ``` diff --git a/.env.example b/.env.example index 199b292..dc1e402 100644 --- a/.env.example +++ b/.env.example @@ -23,13 +23,17 @@ GROQ_API_KEY= # Voice transcription (Groq Whisper, free: ASSISTANT_NAME=claude # Trigger name (@claude) # --- Claude model --- -CLAUDE_MODEL=claude-opus-4-6 # Claude model +CLAUDE_MODEL=claude-opus-4-7 # Claude model CLAUDE_EFFORT=high # Claude effort level CLAUDE_THINKING=adaptive # Claude thinking mode # --- Codex model --- -CODEX_MODEL=gpt-5.4 # Codex model +CODEX_MODEL=gpt-5.5 # Codex model CODEX_EFFORT=xhigh # Codex reasoning effort +# Codex fast mode / goals: dashboard Settings → Codex, or ~/.codex/config.toml: +# [features] +# fast_mode = true +# goals = true # /goal — opt-in, default OFF on Codex 0.133+ # --- Usage dashboard --- STATUS_CHANNEL_ID= # Discord channel ID for live status updates @@ -69,11 +73,11 @@ STATUS_CHANNEL_ID= # Discord channel ID for live status updat # --- Per-role model overrides (paired rooms) --- # Override global CLAUDE_MODEL/CODEX_MODEL per role. # Model is injected based on each role's agent type (claude-code → CLAUDE_MODEL, codex → CODEX_MODEL). -# OWNER_MODEL=gpt-5.4 # Owner model override +# OWNER_MODEL=gpt-5.5 # Owner model override # OWNER_EFFORT=xhigh # Owner effort override # OWNER_FALLBACK_ENABLED=true # Fall back to codex on Claude failure (default: true) # -# REVIEWER_MODEL=claude-opus-4-6 # Reviewer model override +# REVIEWER_MODEL=claude-opus-4-7 # Reviewer model override # REVIEWER_EFFORT=high # Reviewer effort override # REVIEWER_FALLBACK_ENABLED=true # Fall back to codex on Claude failure (default: true) # diff --git a/README.md b/README.md index 1cdc3cd..34b1658 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Version](https://img.shields.io/badge/version-0.2.3-blue) ![Claude Agent SDK](https://img.shields.io/badge/Claude_Agent_SDK-0.2.126-blueviolet) -![Codex SDK](https://img.shields.io/badge/Codex_SDK-0.128.0-green) +![Codex SDK](https://img.shields.io/badge/Codex_SDK-0.133.0-green) ![Bun](https://img.shields.io/badge/Bun-1.3+-f9f1e1?logo=bun&logoColor=black) ![Discord](https://img.shields.io/badge/Discord-Tribunal-5865F2?logo=discord&logoColor=white) @@ -118,7 +118,7 @@ Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Owner (ho 현재 runner 번들 기준 버전: - Claude Agent SDK: `@anthropic-ai/claude-agent-sdk@0.2.126` -- Codex SDK/CLI: `@openai/codex@0.128.0` +- Codex SDK/CLI: `@openai/codex@0.133.0` ### 설치 diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx index 544dc87..506e78a 100644 --- a/apps/dashboard/src/App.tsx +++ b/apps/dashboard/src/App.tsx @@ -46,11 +46,6 @@ interface DashboardState { type FreshnessLevel = DashboardFreshness; -type BeforeInstallPromptEvent = Event & { - prompt: () => Promise; - userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>; -}; - const REFRESH_INTERVAL_MS = 15_000; const LOCALE_STORAGE_KEY = 'ejclaw.dashboard.locale.v2'; const DEFAULT_VIEW: DashboardView = 'rooms'; @@ -158,16 +153,6 @@ function freshnessLabel(level: FreshnessLevel, t: Messages): string { return t.pwa.fresh; } -function isStandaloneDisplay(): boolean { - if (typeof window === 'undefined') return false; - const standaloneNavigator = navigator as Navigator & { standalone?: boolean }; - return ( - standaloneNavigator.standalone === true || - (typeof window.matchMedia === 'function' && - window.matchMedia('(display-mode: standalone)').matches) - ); -} - function canUsePwaCore(): boolean { return ( typeof window !== 'undefined' && @@ -280,10 +265,6 @@ function App() { const [online, setOnline] = useState(() => typeof navigator === 'undefined' ? true : navigator.onLine, ); - const [offlineReady, setOfflineReady] = useState(false); - const [installPrompt, setInstallPrompt] = - useState(null); - const [installed, setInstalled] = useState(isStandaloneDisplay); const [taskActionKey, setTaskActionKey] = useState( null, ); @@ -311,8 +292,6 @@ function App() { persistNickname(trimmed); } const t = messages[locale]; - const secureContext = - typeof window === 'undefined' ? true : window.isSecureContext; function setDashboardLocale(nextLocale: Locale) { setLocale(nextLocale); @@ -459,14 +438,6 @@ function App() { } } - async function handleInstallApp() { - if (!installPrompt) return; - await installPrompt.prompt(); - await installPrompt.userChoice; - setInstallPrompt(null); - setInstalled(isStandaloneDisplay()); - } - useEffect(() => { document.documentElement.lang = localeTags[locale]; }, [locale]); @@ -530,58 +501,11 @@ function App() { }, []); useEffect(() => { - if (!import.meta.env.PROD || !canUsePwaCore()) { - setOfflineReady(false); - return; - } + if (!import.meta.env.PROD || !canUsePwaCore()) return; - let cancelled = false; - void navigator.serviceWorker - .register('/sw.js') - .then((registration) => { - if (!cancelled) { - setOfflineReady( - Boolean( - registration.active || - registration.waiting || - registration.installing, - ), - ); - } - return navigator.serviceWorker.ready; - }) - .then(() => { - if (!cancelled) setOfflineReady(true); - }) - .catch(() => { - if (!cancelled) setOfflineReady(false); - }); - - return () => { - cancelled = true; - }; - }, []); - - useEffect(() => { - function handleBeforeInstallPrompt(event: Event) { - event.preventDefault(); - setInstallPrompt(event as BeforeInstallPromptEvent); - } - - function handleInstalled() { - setInstalled(true); - setInstallPrompt(null); - } - - window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); - window.addEventListener('appinstalled', handleInstalled); - return () => { - window.removeEventListener( - 'beforeinstallprompt', - handleBeforeInstallPrompt, - ); - window.removeEventListener('appinstalled', handleInstalled); - }; + void navigator.serviceWorker.register('/sw.js').catch(() => { + /* ignore registration failures */ + }); }, []); useEffect(() => { @@ -640,42 +564,27 @@ function App() { const roomOptions = data ? buildRoomOptions(data.snapshots) : []; const freshness = dashboardFreshness(online, data?.overview.generatedAt); - const canInstall = Boolean(secureContext && installPrompt && !installed); return (
void handleInstallApp()} onNavigate={navigateToView} - onRefresh={() => void refresh(true)} - refreshing={refreshing} - secureContext={secureContext} t={t} />
setDrawerOpen(false)} - onInstall={() => void handleInstallApp()} onNavigate={navigateToView} onOpenDrawer={() => setDrawerOpen(true)} - offlineReady={offlineReady} onRefresh={() => void refresh(true)} - online={online} refreshing={refreshing} - secureContext={secureContext} t={t} /> @@ -729,7 +638,6 @@ function App() {

{t.panels.scheduled}

- {t.panels.promptPreviews}
void; - onRefresh: () => void; - online: boolean; - refreshing: boolean; - secureContext: boolean; - t: Messages; -}) { - const pwaState = pwaStateLabel({ installed, offlineReady, secureContext, t }); - const status = `${online ? t.pwa.online : t.pwa.offline} · ${pwaState}`; - return ( -
-
- - {status} -
-
- {canInstall ? ( - - ) : null} - -
-
- ); -} - export function SideRail({ activeView, - canInstall, data, - installed, - offlineReady, - onInstall, onNavigate, - onRefresh, - online, - refreshing, - secureContext, t, }: { activeView: DashboardView; - canInstall: boolean; data: DashboardNavData | null; - installed: boolean; - offlineReady: boolean; - onInstall: () => void; onNavigate: (view: DashboardView) => void; - onRefresh: () => void; - online: boolean; - refreshing: boolean; - secureContext: boolean; t: Messages; }) { return ( @@ -238,60 +134,38 @@ export function SideRail({ onNavigate={onNavigate} t={t} /> - ); } export function SectionNav({ activeView, - canInstall, data, drawerOpen, freshness, freshnessText, - installed, onCloseDrawer, - onInstall, onNavigate, onOpenDrawer, - offlineReady, onRefresh, - online, refreshing, - secureContext, t, }: { activeView: DashboardView; - canInstall: boolean; data: DashboardNavData | null; drawerOpen: boolean; freshness: DashboardFreshness; freshnessText: string; - installed: boolean; onCloseDrawer: () => void; - onInstall: () => void; onNavigate: (view: DashboardView) => void; onOpenDrawer: () => void; - offlineReady: boolean; onRefresh: () => void; - online: boolean; refreshing: boolean; - secureContext: boolean; t: Messages; }) { const activeLabel = navItems(t).find((item) => item.view === activeView)?.label ?? t.nav.rooms; + const showManualRefresh = freshness !== 'fresh'; return ( <> @@ -312,16 +186,18 @@ export function SectionNav({ {freshnessText} - + {showManualRefresh ? ( + + ) : null} {drawerOpen ? ( @@ -361,17 +237,6 @@ export function SectionNav({ onNavigate={onNavigate} t={t} /> - ) : null} diff --git a/apps/dashboard/src/MoaSettingsPanel.tsx b/apps/dashboard/src/MoaSettingsPanel.tsx index e277612..31a247d 100644 --- a/apps/dashboard/src/MoaSettingsPanel.tsx +++ b/apps/dashboard/src/MoaSettingsPanel.tsx @@ -8,8 +8,10 @@ import { fetchMoaSettings, updateMoaSettings, } from './api'; +import { type Messages } from './i18n'; +import { SettingsSaveBar, SettingsSectionHeading } from './SettingsPanelChrome'; -export function MoaSettingsPanel() { +export function MoaSettingsPanel({ t }: { t: Messages }) { const [config, setConfig] = useState(null); const [draft, setDraft] = useState(null); const [apiKeys, setApiKeys] = useState>({}); @@ -17,6 +19,7 @@ export function MoaSettingsPanel() { const [checking, setChecking] = useState(null); const [error, setError] = useState(null); const [savedAt, setSavedAt] = useState(null); + const section = t.settings.sections.moa; useEffect(() => { let cancelled = false; @@ -119,15 +122,15 @@ export function MoaSettingsPanel() { id="settings-moa" role="tabpanel" > -
- Arbiter references -

MoA 참조 모델

-

Kimi, GLM 같은 외부 참조 모델을 켜고 연결 상태를 바로 확인합니다.

-
+ {error ?

{error}

: null} {!draft ? (

- {busy ? '불러오는 중…' : 'MoA 설정 없음'} + {busy ? t.settings.common.loading : t.settings.moa.empty}

) : ( )}
@@ -166,6 +170,7 @@ function MoaSettingsContent({ onTest, onToggle, savedAt, + t, }: { apiKeys: Record; busy: boolean; @@ -181,6 +186,7 @@ function MoaSettingsContent({ onTest: (name: string) => Promise; onToggle: () => void; savedAt: number | null; + t: Messages; }) { return ( <> @@ -188,8 +194,9 @@ function MoaSettingsContent({ busy={busy} enabled={draft.enabled} onToggle={onToggle} + t={t} /> -
    +
      {draft.models.map((model) => ( ))}
    - void onSave()} + savedHint={t.settings.common.savedRestartHint} + savingLabel={t.settings.common.saving} + showSavedHint={savedAt !== null && !dirty} /> ); } -function formatMoaStatus(status: MoaReferenceStatus | null): string { - if (!status) return '연결 테스트 전'; +function formatMoaStatus( + status: MoaReferenceStatus | null, + t: Messages, +): string { + if (!status) return t.settings.moa.notTested; const at = new Date(status.checkedAt); const checkedAt = Number.isNaN(at.getTime()) ? status.checkedAt : at.toLocaleString('ko-KR'); - if (status.ok) return `정상 · ${checkedAt}`; - return `실패 · ${checkedAt} · ${status.error ?? 'unknown error'}`; + if (status.ok) { + return `${t.settings.moa.statusOk} · ${checkedAt}`; + } + return `${t.settings.moa.statusFail} · ${checkedAt} · ${status.error ?? 'unknown error'}`; } function MoaMasterToggle({ busy, enabled, onToggle, + t, }: { busy: boolean; enabled: boolean; onToggle: () => void; + t: Messages; }) { return (