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:
219
apps/dashboard/src/SettingsModelFields.tsx
Normal file
219
apps/dashboard/src/SettingsModelFields.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import { type ModelConfigSnapshot, type ModelRoleConfig } from './api';
|
||||
import { type Messages } from './i18n';
|
||||
import {
|
||||
type AgentType,
|
||||
type EffortValue,
|
||||
effortValuesForAgent,
|
||||
formatEffortOption,
|
||||
isEffortSupported,
|
||||
isPresetModel,
|
||||
PRESET_MODELS,
|
||||
} from './settings-options';
|
||||
|
||||
const MODEL_ROLES = ['owner', 'reviewer', 'arbiter'] as const;
|
||||
|
||||
export type ModelRole = (typeof MODEL_ROLES)[number];
|
||||
|
||||
function modelRoleLabel(role: ModelRole, t: Messages): string {
|
||||
if (role === 'owner') return t.settings.models.roleOwner;
|
||||
if (role === 'reviewer') return t.settings.models.roleReviewer;
|
||||
return t.settings.models.roleArbiter;
|
||||
}
|
||||
|
||||
function modelRoleHint(role: ModelRole, t: Messages): string {
|
||||
if (role === 'owner') return t.settings.models.roleOwnerHint;
|
||||
if (role === 'reviewer') return t.settings.models.roleReviewerHint;
|
||||
return t.settings.models.roleArbiterHint;
|
||||
}
|
||||
|
||||
function agentTypeLabel(agentType: AgentType, t: Messages): string {
|
||||
return agentType === 'codex'
|
||||
? t.settings.models.agentTypeCodex
|
||||
: t.settings.models.agentTypeClaude;
|
||||
}
|
||||
|
||||
function effortLabel(value: EffortValue, t: Messages): string {
|
||||
if (value === '') return t.settings.models.effortDefault;
|
||||
const key = value as keyof typeof t.settings.models.effortOptions;
|
||||
const localized = t.settings.models.effortOptions[key] ?? value;
|
||||
return formatEffortOption(value, localized);
|
||||
}
|
||||
|
||||
function modelGroupLabel(model: string, t: Messages): string {
|
||||
if ((PRESET_MODELS.codex as readonly string[]).includes(model)) {
|
||||
return t.settings.models.groupCodex;
|
||||
}
|
||||
if ((PRESET_MODELS.claude as readonly string[]).includes(model)) {
|
||||
return t.settings.models.groupClaude;
|
||||
}
|
||||
return t.settings.models.groupCustom;
|
||||
}
|
||||
|
||||
function effortOptionsForRole(
|
||||
draft: ModelConfigSnapshot,
|
||||
role: ModelRole,
|
||||
): readonly EffortValue[] {
|
||||
const agentType = draft.agentTypes[role];
|
||||
if (agentType) return effortValuesForAgent(agentType);
|
||||
return effortValuesForAgent('codex');
|
||||
}
|
||||
|
||||
export function hasUnsupportedModelEffort(draft: ModelConfigSnapshot): boolean {
|
||||
for (const role of MODEL_ROLES) {
|
||||
const agentType = draft.agentTypes[role];
|
||||
if (!agentType) continue;
|
||||
if (!isEffortSupported(agentType, draft[role].effort)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function ModelRoleFields({
|
||||
draft,
|
||||
onChange,
|
||||
t,
|
||||
}: {
|
||||
draft: ModelConfigSnapshot;
|
||||
onChange: (role: ModelRole, patch: Partial<ModelRoleConfig>) => void;
|
||||
t: Messages;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-model-stack">
|
||||
{MODEL_ROLES.map((role) => {
|
||||
const roleConfig = draft[role];
|
||||
const agentType = draft.agentTypes[role];
|
||||
const effortOptions = effortOptionsForRole(draft, role);
|
||||
const effortInvalid =
|
||||
agentType !== null &&
|
||||
roleConfig.effort !== '' &&
|
||||
!isEffortSupported(agentType, roleConfig.effort);
|
||||
const usingCustom =
|
||||
roleConfig.model.trim() !== '' && !isPresetModel(roleConfig.model);
|
||||
|
||||
return (
|
||||
<article className="settings-model-card" key={role}>
|
||||
<header className="settings-model-card-head">
|
||||
<span className="settings-kicker">{role}</span>
|
||||
<strong>{modelRoleLabel(role, t)}</strong>
|
||||
<p className="settings-hint">{modelRoleHint(role, t)}</p>
|
||||
{agentType ? (
|
||||
<p className="settings-inline-meta">
|
||||
{t.settings.models.agentTypeLabel}:{' '}
|
||||
{agentTypeLabel(agentType, t)}
|
||||
</p>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<div className="settings-model-fields">
|
||||
<label className="settings-row">
|
||||
<span className="settings-label">
|
||||
{t.settings.models.modelLabel}
|
||||
</span>
|
||||
<select
|
||||
aria-label={`${modelRoleLabel(role, t)} ${t.settings.models.modelLabel}`}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
if (value === '__custom__') {
|
||||
onChange(role, {
|
||||
model: usingCustom ? roleConfig.model : '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
onChange(role, { model: value });
|
||||
}}
|
||||
value={usingCustom ? '__custom__' : roleConfig.model}
|
||||
>
|
||||
<option value="">{t.settings.models.modelDefault}</option>
|
||||
<optgroup label={t.settings.models.groupCodex}>
|
||||
{PRESET_MODELS.codex.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
<optgroup label={t.settings.models.groupClaude}>
|
||||
{PRESET_MODELS.claude.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
<option value="__custom__">
|
||||
{t.settings.models.modelCustom}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{usingCustom ? (
|
||||
<label className="settings-row">
|
||||
<span className="settings-label">
|
||||
{t.settings.models.modelCustomLabel}
|
||||
</span>
|
||||
<input
|
||||
onChange={(event) =>
|
||||
onChange(role, { model: event.target.value })
|
||||
}
|
||||
placeholder={t.settings.models.modelPlaceholder}
|
||||
type="text"
|
||||
value={roleConfig.model}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<label className="settings-row">
|
||||
<span className="settings-label">
|
||||
{t.settings.models.effortLabel}
|
||||
</span>
|
||||
<select
|
||||
aria-describedby={
|
||||
effortInvalid ? `settings-effort-warn-${role}` : undefined
|
||||
}
|
||||
aria-invalid={effortInvalid || undefined}
|
||||
aria-label={`${modelRoleLabel(role, t)} ${t.settings.models.effortLabel}`}
|
||||
onChange={(event) =>
|
||||
onChange(role, { effort: event.target.value })
|
||||
}
|
||||
value={roleConfig.effort}
|
||||
>
|
||||
{effortOptions.map((value) => (
|
||||
<option key={value || 'default'} value={value}>
|
||||
{effortLabel(value, t)}
|
||||
</option>
|
||||
))}
|
||||
{effortInvalid ? (
|
||||
<option value={roleConfig.effort}>
|
||||
{effortLabel(roleConfig.effort as EffortValue, t)}
|
||||
</option>
|
||||
) : null}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{effortInvalid && agentType ? (
|
||||
<p
|
||||
className="settings-effort-warn"
|
||||
id={`settings-effort-warn-${role}`}
|
||||
role="alert"
|
||||
>
|
||||
{t.settings.models.effortInvalid
|
||||
.replace('{value}', roleConfig.effort)
|
||||
.replace('{agent}', agentTypeLabel(agentType, t))}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{roleConfig.model || roleConfig.effort ? (
|
||||
<p className="settings-inline-meta">
|
||||
{roleConfig.model
|
||||
? modelGroupLabel(roleConfig.model, t)
|
||||
: t.settings.models.modelDefault}
|
||||
{' · '}
|
||||
{roleConfig.effort
|
||||
? effortLabel(roleConfig.effort as EffortValue, t)
|
||||
: t.settings.models.effortDefault}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user