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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user