diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx
index 162485c..9e7221b 100644
--- a/apps/dashboard/src/App.tsx
+++ b/apps/dashboard/src/App.tsx
@@ -24,6 +24,7 @@ import {
type DashboardTaskAction,
type DashboardOverview,
type DashboardTask,
+ type FastModeSnapshot,
type ModelConfigSnapshot,
type ModelRoleConfig,
type UpdateScheduledTaskInput,
@@ -33,11 +34,13 @@ import {
deleteAccount,
fetchAccounts,
fetchDashboardData,
+ fetchFastMode,
fetchModelConfig,
runInboxAction,
runServiceAction,
runScheduledTaskAction,
sendRoomMessage,
+ updateFastMode,
updateModels,
updateScheduledTask,
} from './api';
@@ -1098,6 +1101,8 @@ function SettingsPanel({
+
+
);
@@ -1234,6 +1239,90 @@ function ModelSettings({ onRestartStack }: { onRestartStack: () => void }) {
);
}
+function FastModeSettings() {
+ const [state, setState] = useState(null);
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ fetchFastMode()
+ .then((s) => {
+ if (cancelled) return;
+ setState(s);
+ setError(null);
+ })
+ .catch((err) => {
+ if (cancelled) return;
+ setError(err instanceof Error ? err.message : String(err));
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ async function toggle(provider: keyof FastModeSnapshot) {
+ if (!state) return;
+ const next = !state[provider];
+ const optimistic = { ...state, [provider]: next };
+ setState(optimistic);
+ setBusy(true);
+ setError(null);
+ try {
+ const fresh = await updateFastMode({ [provider]: next });
+ setState(fresh);
+ } catch (err) {
+ setState(state);
+ setError(err instanceof Error ? err.message : String(err));
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ return (
+
+ );
+}
+
function formatExpiry(
iso: string | null,
): { label: string; cls: string } | null {
diff --git a/apps/dashboard/src/api.ts b/apps/dashboard/src/api.ts
index a62416d..c804eca 100644
--- a/apps/dashboard/src/api.ts
+++ b/apps/dashboard/src/api.ts
@@ -373,6 +373,11 @@ export interface ModelConfigSnapshot {
arbiter: ModelRoleConfig;
}
+export interface FastModeSnapshot {
+ codex: boolean;
+ claude: boolean;
+}
+
export async function fetchAccounts(): Promise<{
claude: ClaudeAccountSummary[];
codex: CodexAccountSummary[];
@@ -412,6 +417,34 @@ export async function updateModels(
return (await response.json()) as ModelConfigSnapshot;
}
+export async function fetchFastMode(): Promise {
+ return fetchJson('/api/settings/fast-mode');
+}
+
+export async function updateFastMode(
+ input: Partial,
+): Promise {
+ 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 deleteAccount(
provider: 'claude' | 'codex',
index: number,
diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css
index 4824dd1..b0cd849 100644
--- a/apps/dashboard/src/styles.css
+++ b/apps/dashboard/src/styles.css
@@ -814,6 +814,40 @@ button:disabled {
margin-top: 8px;
}
+.settings-toggle-row {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 12px;
+ align-items: center;
+ padding: 10px 12px;
+ border: 1px solid var(--panel-border);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.settings-toggle-row + .settings-toggle-row {
+ margin-top: 6px;
+}
+
+.settings-toggle-label {
+ display: grid;
+ gap: 4px;
+ min-width: 0;
+}
+
+.settings-toggle-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--bg-ink);
+}
+
+.settings-toggle-row input[type='checkbox'] {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ accent-color: var(--accent);
+}
+
.settings-save,
.settings-restart {
min-height: 32px;
@@ -874,18 +908,18 @@ button:disabled {
}
.settings-account-main {
- display: flex;
- flex-wrap: wrap;
- gap: 6px 10px;
+ display: grid;
+ grid-template-columns: 28px minmax(0, 1fr) auto auto;
+ gap: 10px;
align-items: center;
min-width: 0;
}
.settings-account-tag {
- flex: none;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 11px;
color: var(--muted);
+ text-align: right;
}
.settings-account-email {
@@ -895,7 +929,7 @@ button:disabled {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- max-width: 100%;
+ min-width: 0;
}
.settings-account-plan {
diff --git a/src/settings-store.ts b/src/settings-store.ts
index 11954fb..7c0cec5 100644
--- a/src/settings-store.ts
+++ b/src/settings-store.ts
@@ -45,6 +45,11 @@ export interface ModelConfigSnapshot {
arbiter: ModelRoleConfig;
}
+export interface FastModeSnapshot {
+ codex: boolean;
+ claude: boolean;
+}
+
const ROLE_KEYS = ['OWNER', 'REVIEWER', 'ARBITER'] as const;
type RoleKey = (typeof ROLE_KEYS)[number];
@@ -272,6 +277,99 @@ export function updateModelConfig(
return getModelConfig();
}
+function codexConfigPath(): string {
+ return path.join(os.homedir(), '.codex', 'config.toml');
+}
+
+function claudeSettingsPath(): string {
+ return path.join(os.homedir(), '.claude', 'settings.json');
+}
+
+function readCodexFastMode(): boolean {
+ const file = codexConfigPath();
+ if (!fs.existsSync(file)) return false;
+ const content = fs.readFileSync(file, 'utf-8');
+ // [features] section, look for fast_mode = true|false
+ const featuresMatch = content.match(/\[features\][\s\S]*?(?=^\[|\Z)/m);
+ const block = featuresMatch ? featuresMatch[0] : content;
+ const m = block.match(/^\s*fast_mode\s*=\s*(true|false)\s*$/m);
+ return m ? m[1] === 'true' : false;
+}
+
+function writeCodexFastMode(value: boolean): void {
+ const file = codexConfigPath();
+ let content = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
+ if (/^\s*fast_mode\s*=\s*(true|false)\s*$/m.test(content)) {
+ content = content.replace(
+ /^\s*fast_mode\s*=\s*(true|false)\s*$/m,
+ `fast_mode = ${value}`,
+ );
+ } else if (/^\[features\]/m.test(content)) {
+ content = content.replace(
+ /^\[features\]\s*$/m,
+ `[features]\nfast_mode = ${value}`,
+ );
+ } else {
+ const trimmed = content.replace(/\s*$/, '');
+ content = `${trimmed}\n\n[features]\nfast_mode = ${value}\n`;
+ }
+ const tempPath = `${file}.tmp`;
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.writeFileSync(tempPath, content, { mode: 0o600 });
+ fs.renameSync(tempPath, file);
+}
+
+function readClaudeFastMode(): boolean {
+ const file = claudeSettingsPath();
+ if (!fs.existsSync(file)) return false;
+ try {
+ const data = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<
+ string,
+ unknown
+ >;
+ return data.fastMode === true;
+ } catch {
+ return false;
+ }
+}
+
+function writeClaudeFastMode(value: boolean): void {
+ const file = claudeSettingsPath();
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ let data: Record = {};
+ if (fs.existsSync(file)) {
+ try {
+ data = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<
+ string,
+ unknown
+ >;
+ } catch {
+ data = {};
+ }
+ }
+ data.fastMode = value;
+ const tempPath = `${file}.tmp`;
+ fs.writeFileSync(tempPath, `${JSON.stringify(data, null, 2)}\n`, {
+ mode: 0o600,
+ });
+ fs.renameSync(tempPath, file);
+}
+
+export function getFastMode(): FastModeSnapshot {
+ return {
+ codex: readCodexFastMode(),
+ claude: readClaudeFastMode(),
+ };
+}
+
+export function updateFastMode(
+ input: Partial,
+): FastModeSnapshot {
+ if (typeof input.codex === 'boolean') writeCodexFastMode(input.codex);
+ if (typeof input.claude === 'boolean') writeClaudeFastMode(input.claude);
+ return getFastMode();
+}
+
export function removeAccountDirectory(
provider: 'claude' | 'codex',
index: number,
diff --git a/src/web-dashboard-server.ts b/src/web-dashboard-server.ts
index 34bf1e6..6d2c990 100644
--- a/src/web-dashboard-server.ts
+++ b/src/web-dashboard-server.ts
@@ -48,10 +48,12 @@ import {
} from './web-dashboard-data.js';
import {
addClaudeAccountFromToken,
+ getFastMode,
getModelConfig,
listClaudeAccounts,
listCodexAccounts,
removeAccountDirectory,
+ updateFastMode,
updateModelConfig,
} from './settings-store.js';
@@ -1271,6 +1273,31 @@ export function createWebDashboardHandler(
}
}
+ if (
+ url.pathname === '/api/settings/fast-mode' &&
+ (request.method === 'PUT' || request.method === 'PATCH')
+ ) {
+ let body: unknown = null;
+ try {
+ body = await request.json();
+ } catch {
+ return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 });
+ }
+ if (!body || typeof body !== 'object') {
+ return jsonResponse(
+ { error: 'Body must be a JSON object' },
+ { status: 400 },
+ );
+ }
+ try {
+ const next = updateFastMode(body as Record);
+ return jsonResponse(next);
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ return jsonResponse({ error: message }, { status: 500 });
+ }
+ }
+
{
const accountAddMatch = url.pathname.match(
/^\/api\/settings\/accounts\/(claude)$/,
@@ -1518,6 +1545,10 @@ export function createWebDashboardHandler(
return jsonResponse(getModelConfig());
}
+ if (url.pathname === '/api/settings/fast-mode') {
+ return jsonResponse(getFastMode());
+ }
+
if (url.pathname === '/api/stream') {
const encoder = new TextEncoder();
const stream = new ReadableStream({