Files
EJClaw/apps/dashboard/src/api.ts
Eyejoker fd3145e2a7 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>
2026-05-23 12:18:42 +09:00

834 lines
21 KiB
TypeScript

export interface DashboardOverview {
generatedAt: string;
services: Array<{
serviceId: string;
assistantName: string;
agentType: string;
updatedAt: string;
totalRooms: number;
activeRooms: number;
}>;
rooms: {
total: number;
active: number;
waiting: number;
inactive: number;
};
tasks: {
total: number;
active: number;
paused: number;
completed: number;
watchers: {
active: number;
paused: number;
completed: number;
};
};
usage: {
rows: Array<{
name: string;
h5pct: number;
h5reset: string;
d7pct: number;
d7reset: string;
}>;
fetchedAt: string | null;
};
operations?: {
serviceRestarts: DashboardServiceRestart[];
};
inbox: Array<{
id: string;
groupKey: string;
kind:
| 'pending-room'
| 'reviewer-request'
| 'approval'
| 'arbiter-request'
| 'ci-failure'
| 'mention';
severity: 'info' | 'warn' | 'error';
title: string;
summary: string;
occurredAt: string;
lastOccurredAt: string;
createdAt: string;
occurrences: number;
source: 'status-snapshot' | 'paired-task' | 'scheduled-task';
roomJid?: string;
roomName?: string;
groupFolder?: string;
serviceId?: string;
taskId?: string;
taskStatus?: string;
}>;
}
export interface DashboardServiceRestart {
id: string;
target: 'stack';
requestedAt: string;
completedAt: string | null;
status: 'running' | 'success' | 'failed';
services: string[];
error?: string;
}
export interface StatusSnapshot {
serviceId: string;
agentType: string;
assistantName: string;
updatedAt: string;
entries: Array<{
jid: string;
name: string;
folder: string;
agentType: string;
status: 'processing' | 'waiting' | 'inactive';
elapsedMs: number | null;
pendingMessages: boolean;
pendingTasks: number;
}>;
}
export interface DashboardRoomActivity {
serviceId: string;
jid: string;
name: string;
folder: string;
agentType: string;
status: 'processing' | 'waiting' | 'inactive';
elapsedMs: number | null;
pendingMessages: boolean;
pendingTasks: number;
messages: Array<{
id: string;
sender: string;
senderName: string;
content: string;
attachments?: Array<{
path: string;
name?: string;
mime?: string;
}>;
timestamp: string;
isFromMe: boolean;
isBotMessage: boolean;
sourceKind: string;
}>;
pairedTask: {
id: string;
title: string | null;
status: string;
roundTripCount: number;
updatedAt: string;
currentTurn: {
turnId: string;
role: string;
intentKind: string;
state: string;
attemptNo: number;
executorServiceId: string | null;
executorAgentType: string | null;
activeRunId: string | null;
createdAt: string;
updatedAt: string;
completedAt: string | null;
lastError: string | null;
progressText: string | null;
progressUpdatedAt: string | null;
} | null;
outputs: Array<{
id: number;
turnNumber: number;
role: string;
verdict: string | null;
createdAt: string;
outputText: string;
attachments?: Array<{
path: string;
name?: string;
mime?: string;
}>;
}>;
} | null;
}
export interface DashboardTask {
id: string;
groupFolder: string;
chatJid: string;
agentType: string | null;
ciProvider: string | null;
ciMetadata: string | null;
scheduleType: string;
scheduleValue: string;
contextMode: string;
nextRun: string | null;
lastRun: string | null;
lastResult: string | null;
status: 'active' | 'paused' | 'completed';
suspendedUntil: string | null;
createdAt: string;
promptPreview: string;
promptLength: number;
isWatcher: boolean;
}
export type DashboardTaskAction = 'pause' | 'resume' | 'cancel';
export type DashboardInboxAction = 'run' | 'decline' | 'dismiss';
export type DashboardServiceAction = 'restart';
export type DashboardTaskScheduleType = 'cron' | 'interval' | 'once';
export type DashboardTaskContextMode = 'group' | 'isolated';
export interface CreateScheduledTaskInput {
roomJid: string;
prompt: string;
scheduleType: DashboardTaskScheduleType;
scheduleValue: string;
contextMode: DashboardTaskContextMode;
requestId?: string;
}
export interface UpdateScheduledTaskInput {
prompt?: string;
scheduleType: DashboardTaskScheduleType;
scheduleValue: string;
}
async function fetchJson<T>(path: string): Promise<T> {
const response = await fetch(path, {
headers: { accept: 'application/json' },
});
if (!response.ok) {
throw new Error(`${path} failed: ${response.status}`);
}
return (await response.json()) as T;
}
async function postJson<T>(path: string, body: unknown): Promise<T> {
const response = await fetch(path, {
method: 'POST',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
let message = `${path} failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) message = payload.error;
} catch {
// Keep the status-based message when the server body is not JSON.
}
throw new Error(message);
}
return (await response.json()) as T;
}
export async function fetchDashboardData(): Promise<{
overview: DashboardOverview;
snapshots: StatusSnapshot[];
tasks: DashboardTask[];
}> {
const [overview, snapshots, tasks] = await Promise.all([
fetchJson<DashboardOverview>('/api/overview'),
fetchJson<StatusSnapshot[]>('/api/status-snapshots'),
fetchJson<DashboardTask[]>('/api/tasks'),
]);
return { overview, snapshots, tasks };
}
export async function fetchRoomTimeline(
roomJid: string,
): Promise<DashboardRoomActivity> {
return fetchJson<DashboardRoomActivity>(
`/api/rooms/${encodeURIComponent(roomJid)}/timeline`,
);
}
export async function fetchRoomsTimelineBatch(): Promise<
Record<string, DashboardRoomActivity>
> {
return fetchJson<Record<string, DashboardRoomActivity>>(
'/api/rooms-timeline',
);
}
export async function runScheduledTaskAction(
taskId: string,
action: DashboardTaskAction,
): Promise<{
ok: true;
id?: string;
deleted?: boolean;
task?: DashboardTask | null;
}> {
return postJson(`/api/tasks/${encodeURIComponent(taskId)}/actions`, {
action,
});
}
export async function createScheduledTask(
input: CreateScheduledTaskInput,
): Promise<{ ok: true; task: DashboardTask | null }> {
return postJson('/api/tasks', input);
}
export async function updateScheduledTask(
taskId: string,
input: UpdateScheduledTaskInput,
): Promise<{ ok: true; task: DashboardTask | null }> {
const response = await fetch(`/api/tasks/${encodeURIComponent(taskId)}`, {
method: 'PATCH',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let message = `/api/tasks/${taskId} failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) message = payload.error;
} catch {
// Keep the status-based message when the server body is not JSON.
}
throw new Error(message);
}
return (await response.json()) as { ok: true; task: DashboardTask | null };
}
export async function runInboxAction(
inboxId: string,
action: DashboardInboxAction,
options: { requestId?: string; lastOccurredAt?: string } = {},
): Promise<{
ok: true;
id: string;
taskId?: string;
intentKind?: string;
status?: string;
queued?: boolean;
dismissed?: boolean;
duplicate?: boolean;
}> {
return postJson(`/api/inbox/${encodeURIComponent(inboxId)}/actions`, {
action,
lastOccurredAt: options.lastOccurredAt,
requestId: options.requestId,
});
}
export async function runServiceAction(
serviceId: 'stack',
action: DashboardServiceAction,
options: { requestId?: string } = {},
): Promise<{
ok: true;
duplicate?: boolean;
restart: DashboardServiceRestart;
}> {
return postJson(`/api/services/${encodeURIComponent(serviceId)}/actions`, {
action,
requestId: options.requestId,
});
}
export async function sendRoomMessage(
roomJid: string,
text: string,
requestId: string,
nickname?: string | null,
): Promise<{ ok: true; id: string; queued: boolean }> {
return postJson(`/api/rooms/${encodeURIComponent(roomJid)}/messages`, {
requestId,
text,
nickname: nickname ?? undefined,
});
}
export interface ClaudeAccountSummary {
index: number;
expiresAt: number | null;
scopes: string[];
subscriptionType?: string;
rateLimitTier?: string;
exists: boolean;
}
export interface CodexRateLimitWindowSummary {
limitWindowSeconds: number | null;
resetAfterSeconds: number | null;
resetAt: string | null;
usedPercent: number | null;
}
export interface CodexRateLimitSummary {
allowed: boolean | null;
limitReached: boolean | null;
primaryWindow: CodexRateLimitWindowSummary | null;
secondaryWindow: CodexRateLimitWindowSummary | null;
}
export interface CodexAdditionalRateLimitSummary {
limitName: string | null;
meteredFeature: string | null;
rateLimit: CodexRateLimitSummary | null;
}
export interface CodexCreditsSummary {
hasCredits: boolean | null;
overageLimitReached: boolean | null;
unlimited: boolean | null;
}
export interface CodexSpendControlSummary {
reached: boolean | null;
}
export interface CodexLiveStatusSummary {
checkedAt: string;
source: 'wham/usage';
planType: string | null;
email: string | null;
rateLimit: CodexRateLimitSummary | null;
rateLimitReachedType: string | null;
additionalRateLimits: CodexAdditionalRateLimitSummary[];
credits: CodexCreditsSummary | null;
spendControl: CodexSpendControlSummary | null;
}
export interface CodexAccountSummary {
index: number;
accountId: string | null;
email: string | null;
planType: string | null;
subscriptionUntil: string | null;
subscriptionLastChecked: string | null;
subscriptionSource: 'jwt-cache' | null;
liveStatus: CodexLiveStatusSummary | null;
exists: boolean;
}
export interface ModelRoleConfig {
model: string;
effort: string;
}
export interface ModelAgentTypes {
owner: 'claude-code' | 'codex';
reviewer: 'claude-code' | 'codex';
arbiter: 'claude-code' | 'codex' | null;
}
export interface ModelConfigSnapshot {
owner: ModelRoleConfig;
reviewer: ModelRoleConfig;
arbiter: ModelRoleConfig;
agentTypes: ModelAgentTypes;
}
export interface FastModeSnapshot {
codex: boolean;
claude: boolean;
}
export interface CodexFeatureSnapshot {
goals: boolean;
}
export interface RuntimePathSnapshot {
label: string;
path: string;
exists: boolean;
}
export interface RuntimeSkillSummary {
name: string;
description: string | null;
path: string;
}
export interface RuntimeSkillDirSnapshot extends RuntimePathSnapshot {
count: number;
skills: RuntimeSkillSummary[];
}
export interface RuntimeMcpSnapshot {
configPath: RuntimePathSnapshot;
ejclawConfigured: boolean;
serverCount: number;
}
export interface RuntimeAgentInventory {
configFiles: RuntimePathSnapshot[];
skillDirs: RuntimeSkillDirSnapshot[];
mcp: RuntimeMcpSnapshot;
}
export interface RuntimeInventorySnapshot {
generatedAt: string;
projectRoot: string;
dataDir: string;
service: {
id: string;
sessionScope: string;
agentType: string;
};
codex: RuntimeAgentInventory;
claude: RuntimeAgentInventory;
ejclaw: {
runnerSkillDir: RuntimeSkillDirSnapshot;
mcpServer: RuntimePathSnapshot;
};
}
export type RoomSkillScope = 'codex-user' | 'claude-user' | 'runner';
export interface RoomSkillCatalogItem {
id: string;
scope: RoomSkillScope;
name: string;
displayName: string;
description: string | null;
path: string;
agentTypes: Array<'claude-code' | 'codex'>;
}
export interface RoomSkillAgentPolicy {
agentType: 'claude-code' | 'codex';
mode: 'all-enabled' | 'custom';
availableSkillIds: string[];
disabledSkillIds: string[];
explicitEnabledSkillIds: string[];
effectiveEnabledSkillIds: string[];
}
export interface RoomSkillPolicyRoom {
jid: string;
name: string;
folder: string;
roomMode?: 'single' | 'tribunal';
agents: RoomSkillAgentPolicy[];
}
export interface RoomSkillSettingsSnapshot {
generatedAt: string;
catalog: RoomSkillCatalogItem[];
rooms: RoomSkillPolicyRoom[];
}
export interface RoomSkillSettingUpdateInput {
roomJid: string;
agentType: 'claude-code' | 'codex';
skillId: string;
enabled: boolean;
}
export interface MoaReferenceStatus {
model: string;
checkedAt: string;
ok: boolean;
error: string | null;
responseLength?: number;
}
export interface MoaModelSettingsSnapshot {
name: string;
enabled: boolean;
model: string;
baseUrl: string;
apiFormat: 'openai' | 'anthropic';
apiKeyConfigured: boolean;
lastStatus: MoaReferenceStatus | null;
}
export interface MoaSettingsSnapshot {
enabled: boolean;
referenceModels: string[];
models: MoaModelSettingsSnapshot[];
}
export async function fetchAccounts(): Promise<{
claude: ClaudeAccountSummary[];
codex: CodexAccountSummary[];
codexCurrentIndex?: number;
}> {
return fetchJson('/api/settings/accounts');
}
export async function fetchModelConfig(): Promise<ModelConfigSnapshot> {
return fetchJson('/api/settings/models');
}
export async function updateModels(
input: Partial<{
owner: Partial<ModelRoleConfig>;
reviewer: Partial<ModelRoleConfig>;
arbiter: Partial<ModelRoleConfig>;
}>,
): Promise<ModelConfigSnapshot> {
const response = await fetch('/api/settings/models', {
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update models failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as ModelConfigSnapshot;
}
export async function refreshCodexAccount(
index: number,
): Promise<CodexAccountSummary> {
const response = await fetch(
`/api/settings/accounts/codex/${index}/refresh`,
{ method: 'POST' },
);
if (!response.ok) {
let msg = `refresh failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
const json = (await response.json()) as { account: CodexAccountSummary };
return json.account;
}
export async function refreshAllCodexAccounts(): Promise<{
refreshed: number[];
failed: Array<{ index: number; error: string }>;
}> {
const response = await fetch('/api/settings/accounts/codex/refresh-all', {
method: 'POST',
});
if (!response.ok) {
throw new Error(`refresh-all failed: ${response.status}`);
}
return (await response.json()) as {
refreshed: number[];
failed: Array<{ index: number; error: string }>;
};
}
export async function setCurrentCodexAccount(
index: number,
): Promise<{ codexCurrentIndex: number }> {
const response = await fetch('/api/settings/accounts/codex/current', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ index }),
});
if (!response.ok) {
let msg = `switch failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as { codexCurrentIndex: number };
}
export async function fetchFastMode(): Promise<FastModeSnapshot> {
return fetchJson('/api/settings/fast-mode');
}
export async function updateFastMode(
input: Partial<FastModeSnapshot>,
): Promise<FastModeSnapshot> {
const response = await fetch('/api/settings/fast-mode', {
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update fast mode failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as FastModeSnapshot;
}
export async function fetchCodexFeatures(): Promise<CodexFeatureSnapshot> {
return fetchJson('/api/settings/codex-features');
}
export async function fetchRuntimeInventory(): Promise<RuntimeInventorySnapshot> {
return fetchJson('/api/settings/runtime-inventory');
}
export async function fetchRoomSkillSettings(): Promise<RoomSkillSettingsSnapshot> {
return fetchJson('/api/settings/room-skills');
}
export async function updateRoomSkillSetting(
input: RoomSkillSettingUpdateInput,
): Promise<RoomSkillSettingsSnapshot> {
const response = await fetch('/api/settings/room-skills', {
method: 'PATCH',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update room skill failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as RoomSkillSettingsSnapshot;
}
export async function updateCodexFeatures(
input: Partial<CodexFeatureSnapshot>,
): Promise<CodexFeatureSnapshot> {
const response = await fetch('/api/settings/codex-features', {
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update codex features failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as CodexFeatureSnapshot;
}
export async function fetchMoaSettings(): Promise<MoaSettingsSnapshot> {
return fetchJson('/api/settings/moa');
}
export async function updateMoaSettings(input: {
enabled?: boolean;
models?: Array<{
name: string;
enabled?: boolean;
model?: string;
baseUrl?: string;
apiFormat?: 'openai' | 'anthropic';
apiKey?: string;
}>;
}): Promise<MoaSettingsSnapshot> {
const response = await fetch('/api/settings/moa', {
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update MoA settings failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as MoaSettingsSnapshot;
}
export async function checkMoaModel(name: string): Promise<MoaReferenceStatus> {
const response = await fetch('/api/settings/moa/check', {
method: 'POST',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify({ name }),
});
if (!response.ok) {
let msg = `check MoA model failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
const payload = (await response.json()) as {
status: MoaReferenceStatus;
};
return payload.status;
}
export async function deleteAccount(
provider: 'claude' | 'codex',
index: number,
): Promise<{ ok: true; provider: string; index: number }> {
const response = await fetch(`/api/settings/accounts/${provider}/${index}`, {
method: 'DELETE',
});
if (!response.ok) {
let msg = `delete account failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as {
ok: true;
provider: string;
index: number;
};
}
export async function addClaudeAccount(
token: string,
): Promise<{ ok: true; index: number; accountId: string | null }> {
return postJson('/api/settings/accounts/claude', { token });
}