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 <cursoragent@cursor.com>

* style: apply pre-commit formatting after dashboard settings commit

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Eyejoker
2026-05-23 12:18:42 +09:00
committed by GitHub
parent 58e5197dc6
commit fd3145e2a7
37 changed files with 3785 additions and 1910 deletions

View File

@@ -1,48 +1,59 @@
import { useState, type ReactNode } from 'react';
import { LOCALES, languageNames, type Locale, type Messages } from './i18n';
export const SETTINGS_NAV_ITEMS = [
{ targetId: 'settings-general', title: '일반', detail: '표시 · 언어' },
{
targetId: 'settings-models',
title: '모델',
detail: 'owner · reviewer · arbiter',
},
{
targetId: 'settings-runtime',
title: '런타임',
detail: 'skills · MCP · config',
},
{ targetId: 'settings-moa', title: 'MoA', detail: '참조 모델 · 연결 테스트' },
{ targetId: 'settings-codex', title: 'Codex', detail: 'fast mode · /goal' },
{ targetId: 'settings-accounts', title: '계정', detail: 'Claude · Codex' },
{ targetId: 'settings-general', navKey: 'general' },
{ targetId: 'settings-models', navKey: 'models' },
{ targetId: 'settings-runtime', navKey: 'runtime' },
{ targetId: 'settings-moa', navKey: 'moa' },
{ targetId: 'settings-codex', navKey: 'codex' },
{ targetId: 'settings-accounts', navKey: 'accounts' },
] as const;
export type SettingsSectionId = (typeof SETTINGS_NAV_ITEMS)[number]['targetId'];
type SettingsNavKey = (typeof SETTINGS_NAV_ITEMS)[number]['navKey'];
function navItem(t: Messages, navKey: SettingsNavKey) {
return t.settings.nav[navKey];
}
export function SettingsNav({
activeSection,
onSelect,
t,
}: {
activeSection: SettingsSectionId;
onSelect: (section: SettingsSectionId) => void;
t: Messages;
}) {
return (
<div className="settings-nav" aria-label="설정 섹션" role="tablist">
{SETTINGS_NAV_ITEMS.map((item) => (
<button
aria-controls={item.targetId}
aria-selected={activeSection === item.targetId}
data-settings-target={item.targetId}
id={`${item.targetId}-tab`}
key={item.targetId}
onClick={() => onSelect(item.targetId)}
role="tab"
type="button"
>
<strong>{item.title}</strong>
<small>{item.detail}</small>
</button>
))}
<div className="settings-nav-scroll">
<div
className="settings-nav"
aria-label={t.settings.navAria}
role="tablist"
>
{SETTINGS_NAV_ITEMS.map((item) => {
const copy = navItem(t, item.navKey);
return (
<button
aria-controls={item.targetId}
aria-selected={activeSection === item.targetId}
data-settings-target={item.targetId}
id={`${item.targetId}-tab`}
key={item.targetId}
onClick={() => onSelect(item.targetId)}
role="tab"
type="button"
>
<strong>{copy.title}</strong>
<small>{copy.detail}</small>
</button>
);
})}
</div>
</div>
);
}
@@ -60,6 +71,8 @@ export function GeneralSettings({
onNicknameChange: (next: string) => void;
t: Messages;
}) {
const section = t.settings.sections.general;
return (
<section
aria-labelledby="settings-general-tab"
@@ -68,36 +81,38 @@ export function GeneralSettings({
role="tabpanel"
>
<SettingsSectionHeading
detail="Dashboard identity"
title="일반"
description="브라우저에서 보이는 이름과 언어만 즉시 바뀝니다."
description={section.description}
detail={section.kicker}
title={section.title}
/>
<div className="settings-form-grid">
<label className="settings-row">
<span className="settings-label">{t.settings.nicknameLabel}</span>
<input
maxLength={32}
onChange={(event) => onNicknameChange(event.target.value)}
placeholder={t.settings.nicknamePlaceholder}
type="text"
value={nickname}
/>
<small className="settings-hint">{t.settings.nicknameHelp}</small>
</label>
<label className="settings-row">
<span className="settings-label">{t.settings.languageLabel}</span>
<select
aria-label={t.settings.languageLabel}
onChange={(event) => onLocaleChange(event.target.value as Locale)}
value={locale}
>
{LOCALES.map((item) => (
<option key={item} value={item}>
{languageNames[item]}
</option>
))}
</select>
</label>
<div className="settings-card">
<div className="settings-form-grid">
<label className="settings-row">
<span className="settings-label">{t.settings.nicknameLabel}</span>
<input
maxLength={32}
onChange={(event) => onNicknameChange(event.target.value)}
placeholder={t.settings.nicknamePlaceholder}
type="text"
value={nickname}
/>
<small className="settings-hint">{t.settings.nicknameHelp}</small>
</label>
<label className="settings-row">
<span className="settings-label">{t.settings.languageLabel}</span>
<select
aria-label={t.settings.languageLabel}
onChange={(event) => onLocaleChange(event.target.value as Locale)}
value={locale}
>
{LOCALES.map((item) => (
<option key={item} value={item}>
{languageNames[item]}
</option>
))}
</select>
</label>
</div>
</div>
</section>
);
@@ -121,24 +136,131 @@ export function SettingsSectionHeading({
);
}
export function SettingsApplyCard({
onRestartStack,
export function SettingsCard({
title,
description,
actions,
children,
}: {
onRestartStack: () => void;
title?: string;
description?: string;
actions?: ReactNode;
children: ReactNode;
}) {
return (
<section className="settings-apply-card" aria-label="변경 적용">
<section className="settings-card">
{title || description || actions ? (
<header className="settings-card-head">
<div>
{title ? <h4>{title}</h4> : null}
{description ? (
<p className="settings-hint">{description}</p>
) : null}
</div>
{actions ? (
<div className="settings-card-actions">{actions}</div>
) : null}
</header>
) : null}
{children}
</section>
);
}
export function SettingsCollapsible({
title,
description,
defaultOpen = false,
children,
}: {
title: string;
description?: string;
defaultOpen?: boolean;
children: ReactNode;
}) {
const [open, setOpen] = useState(defaultOpen);
return (
<section className={`settings-collapsible${open ? ' is-open' : ''}`}>
<button
aria-expanded={open}
className="settings-collapsible-trigger"
onClick={() => setOpen((value) => !value)}
type="button"
>
<span className="settings-collapsible-text">
<strong>{title}</strong>
{description ? <small>{description}</small> : null}
</span>
<span aria-hidden="true" className="settings-collapsible-chevron">
{open ? '▾' : '▸'}
</span>
</button>
{open ? (
<div className="settings-collapsible-body">{children}</div>
) : null}
</section>
);
}
export function SettingsSaveBar({
busy,
dirty,
label,
savingLabel,
onSave,
savedHint,
showSavedHint,
saveDisabled = false,
}: {
busy: boolean;
dirty: boolean;
label: string;
savingLabel: string;
onSave: () => void;
savedHint?: string;
showSavedHint?: boolean;
saveDisabled?: boolean;
}) {
return (
<div className="settings-actions">
<button
className="settings-save"
disabled={!dirty || busy || saveDisabled}
onClick={onSave}
type="button"
>
{busy ? savingLabel : label}
</button>
{showSavedHint && savedHint ? (
<small className="settings-hint">{savedHint}</small>
) : null}
</div>
);
}
export function SettingsApplyCard({
onRestartStack,
t,
}: {
onRestartStack: () => void;
t: Messages;
}) {
const apply = t.settings.apply;
return (
<section aria-label={apply.aria} className="settings-apply-card">
<div>
<span className="settings-kicker">Apply</span>
<strong> </strong>
<small>, MoA, Codex, .</small>
<span className="settings-kicker">{apply.kicker}</span>
<strong>{apply.title}</strong>
<small>{apply.hint}</small>
</div>
<button
className="settings-restart"
onClick={onRestartStack}
type="button"
>
{apply.restart}
</button>
</section>
);