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,258 +1,85 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
fetchRoomSkillSettings,
fetchRuntimeInventory,
updateRoomSkillSetting,
type RuntimeAgentInventory,
type RuntimeInventorySnapshot,
type RuntimePathSnapshot,
type RuntimeSkillDirSnapshot,
type RoomSkillCatalogItem,
type RoomSkillSettingUpdateInput,
type RoomSkillSettingsSnapshot,
} from './api';
import { SettingsSectionHeading } from './SettingsPanelChrome';
import './RuntimeInventorySettings.css';
import { SettingsCard, SettingsSectionHeading } from './SettingsPanelChrome';
import { type Messages } from './i18n';
function ExistsBadge({ exists }: { exists: boolean }) {
return (
<span className={`settings-account-badge ${exists ? 'is-active' : ''}`}>
{exists ? '감지됨' : '없음'}
</span>
);
function agentLabel(agentType: 'claude-code' | 'codex', t: Messages): string {
return agentType === 'codex'
? t.settings.runtime.agentCodex
: t.settings.runtime.agentClaude;
}
function PathRow({ item }: { item: RuntimePathSnapshot }) {
return (
<li className="runtime-path-row">
<span>
<strong>{item.label}</strong>
<code>{item.path}</code>
</span>
<ExistsBadge exists={item.exists} />
</li>
);
function scopeLabel(scope: RoomSkillCatalogItem['scope'], t: Messages): string {
if (scope === 'codex-user') return t.settings.runtime.scopeCodexUser;
if (scope === 'claude-user') return t.settings.runtime.scopeClaudeUser;
return t.settings.runtime.scopeRunner;
}
function SkillDirCard({ dir }: { dir: RuntimeSkillDirSnapshot }) {
const preview = dir.skills.slice(0, 6);
return (
<article className="runtime-skill-card">
<header>
<div>
<strong>{dir.label}</strong>
<code>{dir.path}</code>
</div>
<span className="settings-account-badge is-active">
{dir.count} skills
</span>
</header>
{preview.length === 0 ? (
<p className="settings-hint"> SKILL.md </p>
) : (
<ul className="runtime-skill-list">
{preview.map((skill) => (
<li key={skill.path}>
<strong>{skill.name}</strong>
{skill.description ? <span>{skill.description}</span> : null}
</li>
))}
</ul>
)}
{dir.count > preview.length ? (
<small className="settings-hint">
{dir.count - preview.length}
</small>
) : null}
</article>
);
function applyOptimisticToggle(
snapshot: RoomSkillSettingsSnapshot,
input: RoomSkillSettingUpdateInput,
): RoomSkillSettingsSnapshot {
const { roomJid, agentType, skillId, enabled } = input;
return {
...snapshot,
rooms: snapshot.rooms.map((room) => {
if (room.jid !== roomJid) return room;
return {
...room,
agents: room.agents.map((agent) => {
if (agent.agentType !== agentType) return agent;
const disabledSkillIds = enabled
? agent.disabledSkillIds.filter((id) => id !== skillId)
: agent.disabledSkillIds.includes(skillId)
? agent.disabledSkillIds
: [...agent.disabledSkillIds, skillId];
const effectiveEnabledSkillIds = enabled
? agent.availableSkillIds.filter(
(id) => !disabledSkillIds.includes(id),
)
: agent.availableSkillIds.filter(
(id) => id !== skillId && !disabledSkillIds.includes(id),
);
return {
...agent,
mode: disabledSkillIds.length === 0 ? 'all-enabled' : 'custom',
disabledSkillIds,
effectiveEnabledSkillIds,
};
}),
};
}),
};
}
function AgentInventoryCard({
title,
inventory,
}: {
title: string;
inventory: RuntimeAgentInventory;
}) {
return (
<article className="runtime-agent-card">
<header className="runtime-card-head">
<h4>{title}</h4>
<span className="settings-account-badge is-active">
MCP {inventory.mcp.ejclawConfigured ? '연결' : '미감지'}
</span>
</header>
<ul className="runtime-path-list">
{inventory.configFiles.map((item) => (
<PathRow item={item} key={item.path} />
))}
<PathRow item={inventory.mcp.configPath} />
</ul>
<p className="settings-hint">
MCP servers {inventory.mcp.serverCount} · EJClaw section{' '}
{inventory.mcp.ejclawConfigured ? '있음' : '없음'}
</p>
<div className="runtime-skill-grid">
{inventory.skillDirs.map((dir) => (
<SkillDirCard dir={dir} key={dir.path} />
))}
</div>
</article>
);
}
function agentLabel(agentType: 'claude-code' | 'codex') {
return agentType === 'codex' ? 'Codex' : 'Claude Code';
}
function RoomSkillPolicyCard({
onToggle,
savingKey,
snapshot,
}: {
onToggle: (input: RoomSkillSettingUpdateInput) => void;
savingKey: string | null;
snapshot: RoomSkillSettingsSnapshot | null;
}) {
if (!snapshot) {
return (
<article className="runtime-agent-card">
<header className="runtime-card-head">
<h4> </h4>
</header>
<p className="settings-hint"> </p>
</article>
);
}
const catalogById = new Map<string, RoomSkillCatalogItem>(
snapshot.catalog.map((skill) => [skill.id, skill]),
);
const roomPreview = snapshot.rooms.slice(0, 8);
return (
<article className="runtime-agent-card">
<header className="runtime-card-head">
<div>
<h4> </h4>
<p className="settings-hint">
. PR에서
enable/disable .
</p>
</div>
<span className="settings-account-badge is-active">
{snapshot.catalog.length} skills · {snapshot.rooms.length} rooms
</span>
</header>
{roomPreview.length === 0 ? (
<p className="settings-hint"> .</p>
) : (
<div className="runtime-room-skill-list">
{roomPreview.map((room) => (
<section className="runtime-room-skill-card" key={room.jid}>
<header>
<div>
<strong>{room.name}</strong>
<span>{room.folder}</span>
</div>
<code>{room.jid}</code>
</header>
<div className="runtime-room-agent-grid">
{room.agents.map((agent) => {
const disabledNames = agent.disabledSkillIds
.map((id) => catalogById.get(id)?.displayName ?? id)
.slice(0, 3);
const enabledIds = new Set(agent.effectiveEnabledSkillIds);
return (
<div
className="runtime-room-agent-policy"
key={`${room.jid}:${agent.agentType}`}
>
<strong>{agentLabel(agent.agentType)}</strong>
<span>
{agent.mode === 'all-enabled'
? '기본 전체 ON'
: `${agent.disabledSkillIds.length}개 OFF`}
</span>
<small>
{agent.effectiveEnabledSkillIds.length} / {' '}
{agent.availableSkillIds.length}
</small>
{disabledNames.length > 0 ? (
<small>OFF: {disabledNames.join(', ')}</small>
) : null}
<div className="runtime-room-skill-toggles">
{agent.availableSkillIds.map((skillId) => {
const skill = catalogById.get(skillId);
const key = `${room.jid}:${agent.agentType}:${skillId}`;
return (
<label
className="runtime-room-skill-toggle"
key={skillId}
>
<input
checked={enabledIds.has(skillId)}
disabled={savingKey !== null}
onChange={(event) =>
onToggle({
roomJid: room.jid,
agentType: agent.agentType,
skillId,
enabled: event.currentTarget.checked,
})
}
type="checkbox"
/>
<span>
<strong>{skill?.displayName ?? skillId}</strong>
<small>
{skill?.scope ?? 'unknown'}
{savingKey === key ? ' · 저장 중' : ''}
</small>
</span>
</label>
);
})}
</div>
</div>
);
})}
</div>
</section>
))}
</div>
)}
{snapshot.rooms.length > roomPreview.length ? (
<small className="settings-hint">
{snapshot.rooms.length - roomPreview.length}
</small>
) : null}
</article>
);
}
export function RuntimeInventorySettings() {
const [snapshot, setSnapshot] = useState<RuntimeInventorySnapshot | null>(
export function RuntimeInventorySettings({ t }: { t: Messages }) {
const [snapshot, setSnapshot] = useState<RoomSkillSettingsSnapshot | null>(
null,
);
const [roomSkillSnapshot, setRoomSkillSnapshot] =
useState<RoomSkillSettingsSnapshot | null>(null);
const [error, setError] = useState<string | null>(null);
const [roomSkillError, setRoomSkillError] = useState<string | null>(null);
const [savingRoomSkillKey, setSavingRoomSkillKey] = useState<string | null>(
null,
);
const [savingKey, setSavingKey] = useState<string | null>(null);
const [selectedRoomJid, setSelectedRoomJid] = useState<string>('');
const [selectedAgentType, setSelectedAgentType] = useState<
'claude-code' | 'codex'
>('codex');
useEffect(() => {
let cancelled = false;
Promise.all([fetchRuntimeInventory(), fetchRoomSkillSettings()])
.then(([value, roomSkills]) => {
fetchRoomSkillSettings()
.then((value) => {
if (cancelled) return;
setSnapshot(value);
setRoomSkillSnapshot(roomSkills);
setError(null);
})
.catch((err) => {
@@ -264,17 +91,65 @@ export function RuntimeInventorySettings() {
};
}, []);
async function handleRoomSkillToggle(input: RoomSkillSettingUpdateInput) {
const selectedRoom = useMemo(
() => snapshot?.rooms.find((room) => room.jid === selectedRoomJid) ?? null,
[snapshot, selectedRoomJid],
);
const selectedAgent = useMemo(
() =>
selectedRoom?.agents.find(
(agent) => agent.agentType === selectedAgentType,
) ?? null,
[selectedRoom, selectedAgentType],
);
useEffect(() => {
if (!snapshot || snapshot.rooms.length === 0) return;
if (!snapshot.rooms.some((room) => room.jid === selectedRoomJid)) {
setSelectedRoomJid(snapshot.rooms[0]?.jid ?? '');
}
}, [snapshot, selectedRoomJid]);
useEffect(() => {
if (!selectedRoom || selectedRoom.agents.length === 0) return;
if (
!selectedRoom.agents.some(
(agent) => agent.agentType === selectedAgentType,
)
) {
setSelectedAgentType(selectedRoom.agents[0]?.agentType ?? 'codex');
}
}, [selectedRoom, selectedAgentType]);
const catalogById = useMemo(
() =>
new Map<string, RoomSkillCatalogItem>(
snapshot?.catalog.map((skill) => [skill.id, skill]) ?? [],
),
[snapshot],
);
async function handleToggle(input: RoomSkillSettingUpdateInput) {
const key = `${input.roomJid}:${input.agentType}:${input.skillId}`;
setSavingRoomSkillKey(key);
setRoomSkillError(null);
setError(null);
setSnapshot((current) =>
current ? applyOptimisticToggle(current, input) : current,
);
setSavingKey(key);
try {
const next = await updateRoomSkillSetting(input);
setRoomSkillSnapshot(next);
setSnapshot(next);
} catch (err) {
setRoomSkillError(err instanceof Error ? err.message : String(err));
setError(err instanceof Error ? err.message : String(err));
try {
setSnapshot(await fetchRoomSkillSettings());
} catch {
/* keep optimistic state if refetch also fails */
}
} finally {
setSavingRoomSkillKey(null);
setSavingKey(null);
}
}
@@ -286,55 +161,112 @@ export function RuntimeInventorySettings() {
role="tabpanel"
>
<SettingsSectionHeading
detail="Runtime inventory"
title="런타임"
description="Codex/Claude Code 설정, 스킬, MCP 연결 상태를 읽기 전용으로 확인합니다."
description={t.settings.sections.runtime.description}
detail={t.settings.sections.runtime.kicker}
title={t.settings.sections.runtime.title}
/>
<p className="settings-hint settings-skill-default-hint">
{t.settings.runtime.defaultHint}
</p>
{error ? <p className="settings-error">{error}</p> : null}
{!snapshot ? (
<p className="settings-hint"> </p>
<p className="settings-hint">{t.settings.common.loading}</p>
) : snapshot.rooms.length === 0 ? (
<SettingsCard title={t.settings.runtime.selectRoomLabel}>
<p className="settings-hint">{t.settings.runtime.emptyRooms}</p>
</SettingsCard>
) : (
<div className="runtime-inventory">
<section className="runtime-summary-card">
<div>
<span className="settings-kicker">Current service</span>
<strong>{snapshot.service.id}</strong>
<small>
{snapshot.service.agentType} · session{' '}
{snapshot.service.sessionScope}
</small>
</div>
<div>
<span className="settings-kicker">Project</span>
<code>{snapshot.projectRoot}</code>
<small>data {snapshot.dataDir}</small>
</div>
</section>
<SettingsCard title={t.settings.runtime.selectRoomLabel}>
<div className="settings-form-grid settings-skill-controls">
<label className="settings-row">
<span className="settings-label">
{t.settings.runtime.selectRoomLabel}
</span>
<select
onChange={(event) => setSelectedRoomJid(event.target.value)}
value={selectedRoomJid}
>
{snapshot.rooms.map((room) => (
<option key={room.jid} value={room.jid}>
{room.name} ({room.folder})
</option>
))}
</select>
</label>
<AgentInventoryCard title="Codex" inventory={snapshot.codex} />
<AgentInventoryCard title="Claude Code" inventory={snapshot.claude} />
{roomSkillError ? (
<p className="settings-error">{roomSkillError}</p>
) : null}
<RoomSkillPolicyCard
onToggle={(input) => {
void handleRoomSkillToggle(input);
}}
savingKey={savingRoomSkillKey}
snapshot={roomSkillSnapshot}
/>
{selectedRoom && selectedRoom.agents.length > 1 ? (
<div className="settings-row">
<span className="settings-label">
{t.settings.runtime.selectAgentLabel}
</span>
<div className="settings-skill-agent-tabs" role="tablist">
{selectedRoom.agents.map((agent) => (
<button
aria-selected={selectedAgentType === agent.agentType}
className={
selectedAgentType === agent.agentType
? 'is-active'
: undefined
}
key={agent.agentType}
onClick={() => setSelectedAgentType(agent.agentType)}
role="tab"
type="button"
>
{agentLabel(agent.agentType, t)}
</button>
))}
</div>
</div>
) : null}
</div>
<article className="runtime-agent-card">
<header className="runtime-card-head">
<h4>EJClaw bridge</h4>
<ExistsBadge exists={snapshot.ejclaw.mcpServer.exists} />
</header>
<ul className="runtime-path-list">
<PathRow item={snapshot.ejclaw.mcpServer} />
<PathRow item={snapshot.ejclaw.runnerSkillDir} />
</ul>
</article>
</div>
{!selectedAgent || selectedAgent.availableSkillIds.length === 0 ? (
<p className="settings-hint">{t.settings.runtime.emptySkills}</p>
) : (
<div className="settings-toggle-stack">
{selectedAgent.availableSkillIds.map((skillId) => {
const skill = catalogById.get(skillId);
const key = `${selectedRoomJid}:${selectedAgent.agentType}:${skillId}`;
const enabled =
selectedAgent.effectiveEnabledSkillIds.includes(skillId);
return (
<label
aria-busy={savingKey === key}
className={`settings-toggle-row${savingKey === key ? ' is-busy' : ''}`}
key={skillId}
>
<span className="settings-toggle-label">
<span className="settings-toggle-title">
{skill?.displayName ?? skillId}
</span>
<small>
{skill ? scopeLabel(skill.scope, t) : null}
{skill?.description ? ` · ${skill.description}` : null}
</small>
</span>
<input
checked={enabled}
disabled={savingKey === key}
onChange={(event) =>
void handleToggle({
roomJid: selectedRoomJid,
agentType: selectedAgent.agentType,
skillId,
enabled: event.currentTarget.checked,
})
}
type="checkbox"
/>
</label>
);
})}
</div>
)}
</SettingsCard>
)}
</section>
);