Settings: add Fast Mode toggle for codex/claude + tidy account row alignment (#46)
Fast mode controls
- Codex: writes [features].fast_mode = true|false in ~/.codex/config.toml.
This matches the in-TUI /fast toggle ("toggle Fast mode to enable
fastest inference with increased plan usage").
- Claude: writes fastMode: bool to ~/.claude/settings.json. Mirrors the
/fast slash command preference (effective on opus-4-6).
UX polish
- Account rows switch from flex-wrap to a fixed grid (tag · email · plan ·
expiry · action) so emails of varying lengths don't cause ragged
baselines between rows.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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({
|
||||
|
||||
<ModelSettings onRestartStack={onRestartStack} />
|
||||
|
||||
<FastModeSettings />
|
||||
|
||||
<AccountSettings onRestartStack={onRestartStack} />
|
||||
</div>
|
||||
);
|
||||
@@ -1234,6 +1239,90 @@ function ModelSettings({ onRestartStack }: { onRestartStack: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function FastModeSettings() {
|
||||
const [state, setState] = useState<FastModeSnapshot | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<section className="settings-section">
|
||||
<h3>패스트 모드</h3>
|
||||
{error ? <p className="settings-error">{error}</p> : null}
|
||||
{!state ? (
|
||||
<p className="settings-hint">불러오는 중…</p>
|
||||
) : (
|
||||
<>
|
||||
<label className="settings-toggle-row">
|
||||
<span className="settings-toggle-label">
|
||||
<span className="settings-toggle-title">Codex (GPT)</span>
|
||||
<small className="settings-hint">
|
||||
~/.codex/config.toml [features].fast_mode — 사용량 더 소모하지만
|
||||
응답이 빨라집니다.
|
||||
</small>
|
||||
</span>
|
||||
<input
|
||||
checked={state.codex}
|
||||
disabled={busy}
|
||||
onChange={() => void toggle('codex')}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
<label className="settings-toggle-row">
|
||||
<span className="settings-toggle-label">
|
||||
<span className="settings-toggle-title">Claude</span>
|
||||
<small className="settings-hint">
|
||||
~/.claude/settings.json fastMode — 인터랙티브 세션의 /fast 와
|
||||
동일 키. opus-4-6 한정으로 동작.
|
||||
</small>
|
||||
</span>
|
||||
<input
|
||||
checked={state.claude}
|
||||
disabled={busy}
|
||||
onChange={() => void toggle('claude')}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function formatExpiry(
|
||||
iso: string | null,
|
||||
): { label: string; cls: string } | null {
|
||||
|
||||
@@ -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<FastModeSnapshot> {
|
||||
return fetchJson('/api/settings/fast-mode');
|
||||
}
|
||||
|
||||
export async function updateFastMode(
|
||||
input: Partial<FastModeSnapshot>,
|
||||
): Promise<FastModeSnapshot> {
|
||||
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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user