From fd3145e2a7fde078f203a5faf96e5a94b6724969 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Sat, 23 May 2026 12:18:42 +0900 Subject: [PATCH] feat: refresh dashboard settings UX and Codex feature toggles (#153) * feat: refresh dashboard settings UX and Codex feature toggles Improve settings/tasks mobile layout, model effort validation by agent type, and preset models for GPT 5.5 and Opus 4.7. Store Codex fast mode and goals in config.toml with Claude fastMode session sync and updated docs for Codex 0.133. Co-authored-by: Cursor * style: apply pre-commit formatting after dashboard settings commit Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .claude/skills/setup/SKILL.md | 2 +- .env.example | 12 +- README.md | 4 +- apps/dashboard/src/App.tsx | 100 +-- apps/dashboard/src/DashboardNav.tsx | 163 +--- apps/dashboard/src/MoaSettingsPanel.tsx | 194 ++-- .../src/RuntimeInventorySettings.css | 226 ----- .../src/RuntimeInventorySettings.tsx | 494 +++++------ apps/dashboard/src/SettingsModelFields.tsx | 219 +++++ apps/dashboard/src/SettingsPanel.test.ts | 12 +- apps/dashboard/src/SettingsPanel.tsx | 479 +++++----- apps/dashboard/src/SettingsPanelChrome.tsx | 256 ++++-- apps/dashboard/src/TaskPanel.css | 755 ++++++++-------- apps/dashboard/src/TaskPanel.test.ts | 3 +- apps/dashboard/src/TaskPanel.tsx | 319 ++++--- apps/dashboard/src/api.ts | 7 + apps/dashboard/src/i18n.ts | 836 +++++++++++++++++- apps/dashboard/src/settings-options.test.ts | 31 + apps/dashboard/src/settings-options.ts | 70 ++ apps/dashboard/src/styles.css | 725 ++++++++++++--- docs/configuration.md | 8 +- scripts/dashboard-ux.ts | 230 ++++- src/agent-runner-environment.test.ts | 19 + src/agent-runner-environment.ts | 45 +- src/channels/discord.test.ts | 16 + src/channels/discord.ts | 4 +- src/claude-session-settings.test.ts | 39 + src/claude-session-settings.ts | 68 ++ src/codex-config-features.test.ts | 30 + src/codex-config-features.ts | 84 ++ src/index.ts | 21 +- src/settings-effort.test.ts | 20 + src/settings-effort.ts | 64 ++ src/settings-store.test.ts | 24 +- src/settings-store.ts | 107 ++- src/web-dashboard-settings-routes.test.ts | 5 + src/web-dashboard-settings-routes.ts | 4 +- 37 files changed, 3785 insertions(+), 1910 deletions(-) delete mode 100644 apps/dashboard/src/RuntimeInventorySettings.css create mode 100644 apps/dashboard/src/SettingsModelFields.tsx create mode 100644 apps/dashboard/src/settings-options.test.ts create mode 100644 apps/dashboard/src/settings-options.ts create mode 100644 src/claude-session-settings.test.ts create mode 100644 src/claude-session-settings.ts create mode 100644 src/codex-config-features.test.ts create mode 100644 src/codex-config-features.ts create mode 100644 src/settings-effort.test.ts create mode 100644 src/settings-effort.ts 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 (