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,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import type {
CreateScheduledTaskInput,
@@ -18,6 +18,7 @@ import './TaskPanel.css';
export type { TaskActionKey } from './TaskActionButtons';
type TaskGroupKey = 'watchers' | 'scheduled' | 'paused' | 'completed';
type TaskFilterKey = TaskGroupKey | 'all';
type TaskResultTone = 'ok' | 'fail' | 'none';
export interface RoomOption {
@@ -317,16 +318,7 @@ function buildTaskSummary(tasks: DashboardTask[]): TaskSummary {
return summary;
}
function TaskSummaryMetric({ label, value }: { label: string; value: number }) {
return (
<span className="task-summary-metric">
<strong>{value}</strong>
<small>{label}</small>
</span>
);
}
function TaskBoardSummary({
function TaskSummaryBar({
locale,
summary,
t,
@@ -337,51 +329,82 @@ function TaskBoardSummary({
}) {
const nextTask = summary.nextTask;
return (
<section className="task-command-center" aria-label={t.panels.scheduled}>
<div className="task-command-copy">
<span className="eyebrow">{t.panels.scheduled}</span>
<strong>
{summary.total} {t.tasks.count}
</strong>
<p>
{nextTask
? `${t.tasks.next}: ${taskHeadline(nextTask, t)}`
: t.tasks.empty}
</p>
</div>
<div className="task-command-next">
<section className="task-summary-bar" aria-label={t.panels.scheduled}>
<div className="task-summary-next">
<small>{t.tasks.next}</small>
<strong>
{nextTask ? formatRelativeDate(nextTask.nextRun, locale, t) : '-'}
</strong>
<span>
{nextTask
? `${nextTask.groupFolder} · ${taskScheduleText(nextTask, t)}`
: t.tasks.noTime}
? taskHeadline(nextTask, t)
: summary.total > 0
? `${summary.total} ${t.tasks.count}`
: t.tasks.empty}
</span>
</div>
<div className="task-command-metrics">
<TaskSummaryMetric
label={t.tasks.groups.scheduled}
value={summary.scheduled}
/>
<TaskSummaryMetric
label={t.tasks.groups.watchers}
value={summary.watchers}
/>
<TaskSummaryMetric
label={t.tasks.groups.paused}
value={summary.paused}
/>
<TaskSummaryMetric
label={t.tasks.groups.completed}
value={summary.completed}
/>
<div className="task-summary-chips" aria-hidden="true">
<span className="task-chip">
{t.tasks.groups.scheduled} {summary.scheduled}
</span>
<span className="task-chip">
{t.tasks.groups.watchers} {summary.watchers}
</span>
<span className="task-chip">
{t.tasks.groups.paused} {summary.paused}
</span>
<span className="task-chip">
{t.tasks.groups.completed} {summary.completed}
</span>
</div>
</section>
);
}
function TaskFilterTabs({
activeFilter,
counts,
onSelect,
t,
}: {
activeFilter: TaskFilterKey;
counts: Record<TaskFilterKey, number>;
onSelect: (filter: TaskFilterKey) => void;
t: Messages;
}) {
const tabs: Array<{ key: TaskFilterKey; label: string }> = [
{ key: 'all', label: t.tasks.filterAll },
{ key: 'scheduled', label: t.tasks.groups.scheduled },
{ key: 'watchers', label: t.tasks.groups.watchers },
{ key: 'paused', label: t.tasks.groups.paused },
{ key: 'completed', label: t.tasks.groups.completed },
];
return (
<div className="task-filter-scroll">
<div
aria-label={t.panels.scheduled}
className="task-filter-tabs"
role="tablist"
>
{tabs.map((tab) => (
<button
aria-selected={activeFilter === tab.key}
className="task-filter-tab"
key={tab.key}
onClick={() => onSelect(tab.key)}
role="tab"
type="button"
>
<strong>{tab.label}</strong>
<small>{counts[tab.key]}</small>
</button>
))}
</div>
</div>
);
}
function TaskCreateForm({
rooms,
onTaskCreate,
@@ -389,32 +412,30 @@ function TaskCreateForm({
t,
}: TaskCreateFormProps) {
return (
<form
className="task-create-form"
onSubmit={(event) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
const input = readTaskForm(form, true);
if (!input) return;
onTaskCreate(input);
event.currentTarget.reset();
}}
>
<div className="task-create-head">
<span className="eyebrow">{t.tasks.createTitle}</span>
<strong>{t.tasks.createSubtitle}</strong>
<p>{t.tasks.scheduleValueHint}</p>
</div>
<div className="task-create-body">
<details className="task-create-panel">
<summary>{t.tasks.createTitle}</summary>
<form
className="task-create-form"
onSubmit={(event) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
const input = readTaskForm(form, true);
if (!input) return;
onTaskCreate(input);
event.currentTarget.reset();
}}
>
<p className="task-create-hint">{t.tasks.scheduleValueHint}</p>
<label className="task-form-wide">
<span>{t.tasks.prompt}</span>
<textarea
name="prompt"
placeholder={t.tasks.promptPlaceholder}
required
rows={3}
/>
</label>
<div className="task-form-controls">
<div className="task-form-grid">
<label>
<span>{t.tasks.room}</span>
<select name="roomJid" required>
@@ -450,16 +471,13 @@ function TaskCreateForm({
</select>
</label>
</div>
<div className="task-form-footer">
<span>{t.tasks.schedule}</span>
<button disabled={taskActionKey === 'create'} type="submit">
{taskActionKey === 'create'
? t.tasks.actions.busy
: t.tasks.actions.create}
</button>
</div>
</div>
</form>
<button disabled={taskActionKey === 'create'} type="submit">
{taskActionKey === 'create'
? t.tasks.actions.busy
: t.tasks.actions.create}
</button>
</form>
</details>
);
}
@@ -515,16 +533,16 @@ function TaskResult({ task, t }: { task: DashboardTask; t: Messages }) {
);
}
function TaskPromptDetails({ task, t }: { task: DashboardTask; t: Messages }) {
function TaskPromptBlock({ task, t }: { task: DashboardTask; t: Messages }) {
return (
<details className="task-prompt">
<summary>{t.tasks.prompt}</summary>
<div className="task-prompt-block">
<span>{t.tasks.prompt}</span>
<p>{safePreview(task.promptPreview, t.tasks.emptyPrompt)}</p>
<small>
{task.id} · {taskScheduleText(task, t)} · {task.promptLength}{' '}
{t.units.chars}
</small>
</details>
</div>
);
}
@@ -599,35 +617,50 @@ function TaskCard({
t,
}: TaskCardProps) {
return (
<article className={`task-card task-card-${groupKey}`}>
<div className="task-card-main">
<div className="task-title">
<span className="task-kind">{taskDisplayName(task, t)}</span>
<strong>{taskHeadline(task, t)}</strong>
<TaskMetaStrip task={task} t={t} />
</div>
<div className="task-status-line">
<article className={`task-row task-row-${groupKey}`}>
<div className="task-row-head">
<div className="task-row-lead">
<span className={`pill pill-${task.status}`}>
{statusLabel(task.status, t)}
</span>
<div className="task-row-copy">
<span className="task-kind">{taskDisplayName(task, t)}</span>
<strong title={taskHeadline(task, t)}>
{taskHeadline(task, t)}
</strong>
<TaskMetaStrip task={task} t={t} />
</div>
</div>
<div className="task-row-aside">
<div className="task-row-next">
<small>{t.tasks.next}</small>
<strong>{formatRelativeDate(task.nextRun, locale, t)}</strong>
<em>{formatTaskDate(task.nextRun, locale)}</em>
</div>
<TaskActionButtons
className="task-row-actions"
onTaskAction={onTaskAction}
task={task}
taskActionKey={taskActionKey}
t={t}
/>
</div>
</div>
<TaskActionButtons
onTaskAction={onTaskAction}
task={task}
taskActionKey={taskActionKey}
t={t}
/>
<TaskTimeGrid locale={locale} t={t} task={task} />
<TaskSuspendedUntil locale={locale} t={t} task={task} />
<TaskResult task={task} t={t} />
<TaskPromptDetails task={task} t={t} />
<TaskEditForm
onTaskUpdate={onTaskUpdate}
task={task}
taskActionKey={taskActionKey}
t={t}
/>
<details className="task-row-details">
<summary>{t.tasks.details}</summary>
<div className="task-row-details-body">
<TaskTimeGrid locale={locale} t={t} task={task} />
<TaskSuspendedUntil locale={locale} t={t} task={task} />
<TaskResult task={task} t={t} />
<TaskPromptBlock task={task} t={t} />
<TaskEditForm
onTaskUpdate={onTaskUpdate}
task={task}
taskActionKey={taskActionKey}
t={t}
/>
</div>
</details>
</article>
);
}
@@ -673,34 +706,41 @@ function TaskGroupHead({
}) {
return (
<div className="task-group-head">
<div>
<span className="eyebrow">{label}</span>
<strong>
{group.tasks.length} {countLabel}
</strong>
</div>
<span className={`pill pill-${group.key}`}>{group.tasks.length}</span>
<strong>
{label} · {group.tasks.length} {countLabel}
</strong>
</div>
);
}
function TaskGroupSection(props: TaskGroupSectionProps) {
const { group, t } = props;
function TaskGroupSection({
group,
locale,
onTaskAction,
onTaskUpdate,
showHead,
taskActionKey,
t,
}: TaskGroupSectionProps & { showHead: boolean }) {
if (group.tasks.length === 0) return null;
const label = t.tasks.groups[group.key];
const body = <TaskGroupBody {...props} />;
const body = (
<TaskGroupBody
group={group}
locale={locale}
onTaskAction={onTaskAction}
onTaskUpdate={onTaskUpdate}
taskActionKey={taskActionKey}
t={t}
/>
);
if (group.key === 'completed') {
return (
<details className="task-group task-group-completed">
<summary className="task-group-head">
<div>
<span className="eyebrow">{label}</span>
<strong>
{group.tasks.length} {t.tasks.count}
</strong>
</div>
<strong>{label}</strong>
<span className={`pill pill-${group.key}`}>{group.tasks.length}</span>
</summary>
{body}
@@ -708,6 +748,12 @@ function TaskGroupSection(props: TaskGroupSectionProps) {
);
}
if (!showHead) {
return (
<section className={`task-group task-group-${group.key}`}>{body}</section>
);
}
return (
<section className={`task-group task-group-${group.key}`}>
<TaskGroupHead countLabel={t.tasks.count} group={group} label={label} />
@@ -716,6 +762,21 @@ function TaskGroupSection(props: TaskGroupSectionProps) {
);
}
function buildFilterCounts(groups: TaskGroup[]): Record<TaskFilterKey, number> {
const counts: Record<TaskFilterKey, number> = {
all: 0,
watchers: 0,
scheduled: 0,
paused: 0,
completed: 0,
};
for (const group of groups) {
counts[group.key] = group.tasks.length;
counts.all += group.tasks.length;
}
return counts;
}
export function TaskPanel({
tasks,
rooms,
@@ -728,26 +789,48 @@ export function TaskPanel({
}: TaskPanelProps) {
const taskGroups = useMemo(() => buildTaskGroups(tasks), [tasks]);
const taskSummary = useMemo(() => buildTaskSummary(tasks), [tasks]);
const filterCounts = useMemo(
() => buildFilterCounts(taskGroups),
[taskGroups],
);
const [activeFilter, setActiveFilter] = useState<TaskFilterKey>('all');
const visibleGroups = useMemo(() => {
if (activeFilter === 'all') {
return taskGroups.filter((group) => group.tasks.length > 0);
}
return taskGroups.filter((group) => group.key === activeFilter);
}, [activeFilter, taskGroups]);
return (
<div className="task-board" aria-label={t.tasks.cardsAria}>
<TaskBoardSummary locale={locale} summary={taskSummary} t={t} />
<TaskSummaryBar locale={locale} summary={taskSummary} t={t} />
<TaskFilterTabs
activeFilter={activeFilter}
counts={filterCounts}
onSelect={setActiveFilter}
t={t}
/>
<TaskCreateForm
rooms={rooms}
onTaskCreate={onTaskCreate}
rooms={rooms}
taskActionKey={taskActionKey}
t={t}
/>
{tasks.length === 0 ? <EmptyState>{t.tasks.empty}</EmptyState> : null}
{tasks.length > 0 && visibleGroups.length === 0 ? (
<EmptyState>{t.tasks.groupEmpty}</EmptyState>
) : null}
<div className="task-lanes">
{taskGroups.map((group) => (
{visibleGroups.map((group) => (
<TaskGroupSection
group={group}
key={group.key}
locale={locale}
onTaskAction={onTaskAction}
onTaskUpdate={onTaskUpdate}
showHead={activeFilter === 'all'}
taskActionKey={taskActionKey}
t={t}
/>