diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx index 162485c..9e7221b 100644 --- a/apps/dashboard/src/App.tsx +++ b/apps/dashboard/src/App.tsx @@ -24,6 +24,7 @@ import { type DashboardTaskAction, type DashboardOverview, type DashboardTask, + type FastModeSnapshot, type ModelConfigSnapshot, type ModelRoleConfig, type UpdateScheduledTaskInput, @@ -33,11 +34,13 @@ import { deleteAccount, fetchAccounts, fetchDashboardData, + fetchFastMode, fetchModelConfig, runInboxAction, runServiceAction, runScheduledTaskAction, sendRoomMessage, + updateFastMode, updateModels, updateScheduledTask, } from './api'; @@ -1098,6 +1101,8 @@ function SettingsPanel({ + + ); @@ -1234,6 +1239,90 @@ function ModelSettings({ onRestartStack }: { onRestartStack: () => void }) { ); } +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 formatExpiry( iso: string | null, ): { label: string; cls: string } | null { diff --git a/apps/dashboard/src/api.ts b/apps/dashboard/src/api.ts index a62416d..c804eca 100644 --- a/apps/dashboard/src/api.ts +++ b/apps/dashboard/src/api.ts @@ -373,6 +373,11 @@ export interface ModelConfigSnapshot { arbiter: ModelRoleConfig; } +export interface FastModeSnapshot { + codex: boolean; + claude: boolean; +} + export async function fetchAccounts(): Promise<{ claude: ClaudeAccountSummary[]; codex: CodexAccountSummary[]; @@ -412,6 +417,34 @@ export async function updateModels( return (await response.json()) as ModelConfigSnapshot; } +export async function fetchFastMode(): Promise { + return fetchJson('/api/settings/fast-mode'); +} + +export async function updateFastMode( + input: Partial, +): Promise { + const response = await fetch('/api/settings/fast-mode', { + method: 'PUT', + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + body: JSON.stringify(input), + }); + if (!response.ok) { + let msg = `update fast mode failed: ${response.status}`; + try { + const payload = (await response.json()) as { error?: string }; + if (payload.error) msg = payload.error; + } catch { + /* ignore */ + } + throw new Error(msg); + } + return (await response.json()) as FastModeSnapshot; +} + export async function deleteAccount( provider: 'claude' | 'codex', index: number, diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css index 4824dd1..b0cd849 100644 --- a/apps/dashboard/src/styles.css +++ b/apps/dashboard/src/styles.css @@ -814,6 +814,40 @@ button:disabled { margin-top: 8px; } +.settings-toggle-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: center; + padding: 10px 12px; + border: 1px solid var(--panel-border); + border-radius: 8px; + background: rgba(255, 255, 255, 0.02); +} + +.settings-toggle-row + .settings-toggle-row { + margin-top: 6px; +} + +.settings-toggle-label { + display: grid; + gap: 4px; + min-width: 0; +} + +.settings-toggle-title { + font-size: 13px; + font-weight: 600; + color: var(--bg-ink); +} + +.settings-toggle-row input[type='checkbox'] { + width: 18px; + height: 18px; + cursor: pointer; + accent-color: var(--accent); +} + .settings-save, .settings-restart { min-height: 32px; @@ -874,18 +908,18 @@ button:disabled { } .settings-account-main { - display: flex; - flex-wrap: wrap; - gap: 6px 10px; + display: grid; + grid-template-columns: 28px minmax(0, 1fr) auto auto; + gap: 10px; align-items: center; min-width: 0; } .settings-account-tag { - flex: none; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 11px; color: var(--muted); + text-align: right; } .settings-account-email { @@ -895,7 +929,7 @@ button:disabled { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-width: 100%; + min-width: 0; } .settings-account-plan { diff --git a/src/settings-store.ts b/src/settings-store.ts index 11954fb..7c0cec5 100644 --- a/src/settings-store.ts +++ b/src/settings-store.ts @@ -45,6 +45,11 @@ export interface ModelConfigSnapshot { arbiter: ModelRoleConfig; } +export interface FastModeSnapshot { + codex: boolean; + claude: boolean; +} + const ROLE_KEYS = ['OWNER', 'REVIEWER', 'ARBITER'] as const; type RoleKey = (typeof ROLE_KEYS)[number]; @@ -272,6 +277,99 @@ export function updateModelConfig( return getModelConfig(); } +function codexConfigPath(): string { + return path.join(os.homedir(), '.codex', 'config.toml'); +} + +function claudeSettingsPath(): string { + return path.join(os.homedir(), '.claude', 'settings.json'); +} + +function readCodexFastMode(): boolean { + const file = codexConfigPath(); + if (!fs.existsSync(file)) return false; + const content = fs.readFileSync(file, 'utf-8'); + // [features] section, look for fast_mode = true|false + const featuresMatch = content.match(/\[features\][\s\S]*?(?=^\[|\Z)/m); + const block = featuresMatch ? featuresMatch[0] : content; + const m = block.match(/^\s*fast_mode\s*=\s*(true|false)\s*$/m); + return m ? m[1] === 'true' : false; +} + +function writeCodexFastMode(value: boolean): void { + const file = codexConfigPath(); + let content = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : ''; + if (/^\s*fast_mode\s*=\s*(true|false)\s*$/m.test(content)) { + content = content.replace( + /^\s*fast_mode\s*=\s*(true|false)\s*$/m, + `fast_mode = ${value}`, + ); + } else if (/^\[features\]/m.test(content)) { + content = content.replace( + /^\[features\]\s*$/m, + `[features]\nfast_mode = ${value}`, + ); + } else { + const trimmed = content.replace(/\s*$/, ''); + content = `${trimmed}\n\n[features]\nfast_mode = ${value}\n`; + } + const tempPath = `${file}.tmp`; + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(tempPath, content, { mode: 0o600 }); + fs.renameSync(tempPath, file); +} + +function readClaudeFastMode(): boolean { + const file = claudeSettingsPath(); + if (!fs.existsSync(file)) return false; + try { + const data = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record< + string, + unknown + >; + return data.fastMode === true; + } catch { + return false; + } +} + +function writeClaudeFastMode(value: boolean): void { + const file = claudeSettingsPath(); + fs.mkdirSync(path.dirname(file), { recursive: true }); + let data: Record = {}; + if (fs.existsSync(file)) { + try { + data = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record< + string, + unknown + >; + } catch { + data = {}; + } + } + data.fastMode = value; + const tempPath = `${file}.tmp`; + fs.writeFileSync(tempPath, `${JSON.stringify(data, null, 2)}\n`, { + mode: 0o600, + }); + fs.renameSync(tempPath, file); +} + +export function getFastMode(): FastModeSnapshot { + return { + codex: readCodexFastMode(), + claude: readClaudeFastMode(), + }; +} + +export function updateFastMode( + input: Partial, +): FastModeSnapshot { + if (typeof input.codex === 'boolean') writeCodexFastMode(input.codex); + if (typeof input.claude === 'boolean') writeClaudeFastMode(input.claude); + return getFastMode(); +} + export function removeAccountDirectory( provider: 'claude' | 'codex', index: number, diff --git a/src/web-dashboard-server.ts b/src/web-dashboard-server.ts index 34bf1e6..6d2c990 100644 --- a/src/web-dashboard-server.ts +++ b/src/web-dashboard-server.ts @@ -48,10 +48,12 @@ import { } from './web-dashboard-data.js'; import { addClaudeAccountFromToken, + getFastMode, getModelConfig, listClaudeAccounts, listCodexAccounts, removeAccountDirectory, + updateFastMode, updateModelConfig, } from './settings-store.js'; @@ -1271,6 +1273,31 @@ export function createWebDashboardHandler( } } + if ( + url.pathname === '/api/settings/fast-mode' && + (request.method === 'PUT' || request.method === 'PATCH') + ) { + let body: unknown = null; + try { + body = await request.json(); + } catch { + return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 }); + } + if (!body || typeof body !== 'object') { + return jsonResponse( + { error: 'Body must be a JSON object' }, + { status: 400 }, + ); + } + try { + const next = updateFastMode(body as Record); + return jsonResponse(next); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return jsonResponse({ error: message }, { status: 500 }); + } + } + { const accountAddMatch = url.pathname.match( /^\/api\/settings\/accounts\/(claude)$/, @@ -1518,6 +1545,10 @@ export function createWebDashboardHandler( return jsonResponse(getModelConfig()); } + if (url.pathname === '/api/settings/fast-mode') { + return jsonResponse(getFastMode()); + } + if (url.pathname === '/api/stream') { const encoder = new TextEncoder(); const stream = new ReadableStream({