diff --git a/packages/web/src/features/supervisor/actions/use-objective-dialog-state.ts b/packages/web/src/features/supervisor/actions/use-objective-dialog-state.ts
index 040289e52..72327f1ad 100644
--- a/packages/web/src/features/supervisor/actions/use-objective-dialog-state.ts
+++ b/packages/web/src/features/supervisor/actions/use-objective-dialog-state.ts
@@ -18,8 +18,52 @@ const CLOSED_DIALOG_STATE = {
mode: "enable" as const,
draftObjective: "",
draftEvaluatorProviderId: "claude" as const,
+ draftEvaluatorModel: "",
+ draftMaxSupervisionCount: "0",
+ draftScheduledAt: "",
};
+export function formatScheduledAtInput(value?: number): string {
+ if (typeof value !== "number" || !Number.isFinite(value)) {
+ return "";
+ }
+
+ const date = new Date(value);
+ const offsetMinutes = date.getTimezoneOffset();
+ const localDate = new Date(date.getTime() - offsetMinutes * 60_000);
+ return localDate.toISOString().slice(0, 16);
+}
+
+function parseDraftMaxSupervisionCount(value: string): number {
+ const parsed = Number.parseInt(value, 10);
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
+ return 0;
+ }
+ return parsed;
+}
+
+function isValidDraftMaxSupervisionCount(value: string | undefined): boolean {
+ const trimmed = (value ?? "").trim();
+ if (!trimmed) {
+ return false;
+ }
+ const parsed = Number(trimmed);
+ return Number.isSafeInteger(parsed) && parsed >= 0;
+}
+
+function parseDraftScheduledAt(value: string): number | undefined {
+ if (!value.trim()) {
+ return undefined;
+ }
+
+ const parsed = Date.parse(value);
+ if (!Number.isFinite(parsed)) {
+ return undefined;
+ }
+
+ return parsed;
+}
+
interface UseObjectiveDialogStateOptions {
workspaceId: string;
sessionId?: string;
@@ -45,6 +89,9 @@ export function useObjectiveDialogState({
};
const isDisable = mode === "disable";
const disableObjective = supervisor?.objective ?? dialog.draftObjective;
+ const isMaxSupervisionCountValid = isValidDraftMaxSupervisionCount(
+ dialog.draftMaxSupervisionCount
+ );
const close = useCallback(() => {
setDialog(CLOSED_DIALOG_STATE);
@@ -55,6 +102,9 @@ export function useObjectiveDialogState({
patch: Partial<{
draftObjective: string;
draftEvaluatorProviderId: ObjectiveDialogEvaluatorProviderId;
+ draftEvaluatorModel: string;
+ draftMaxSupervisionCount: string;
+ draftScheduledAt: string;
}>
) => {
setDialog((current) => ({ ...current, ...patch }));
@@ -85,12 +135,23 @@ export function useObjectiveDialogState({
return false;
}
+ if (!isMaxSupervisionCountValid) {
+ return false;
+ }
+
+ const evaluatorModel = dialog.draftEvaluatorModel.trim();
+ const maxSupervisionCount = parseDraftMaxSupervisionCount(dialog.draftMaxSupervisionCount);
+ const scheduledAt = parseDraftScheduledAt(dialog.draftScheduledAt);
+
if (dialog.mode === "enable") {
const result = await dispatch("supervisor.create", {
sessionId: dialog.sessionId,
workspaceId,
objective,
evaluatorProviderId: dialog.draftEvaluatorProviderId,
+ evaluatorModel: evaluatorModel || undefined,
+ maxSupervisionCount,
+ scheduledAt,
});
if (result.ok) {
@@ -108,6 +169,9 @@ export function useObjectiveDialogState({
id: supervisor.id,
objective,
evaluatorProviderId: dialog.draftEvaluatorProviderId,
+ evaluatorModel: evaluatorModel || null,
+ maxSupervisionCount,
+ scheduledAt: scheduledAt ?? null,
});
if (result.ok) {
@@ -116,7 +180,7 @@ export function useObjectiveDialogState({
}
return false;
- }, [close, dialog, dispatch, supervisor, workspaceId]);
+ }, [close, dialog, dispatch, isMaxSupervisionCountValid, supervisor, workspaceId]);
return {
dialog,
@@ -126,8 +190,10 @@ export function useObjectiveDialogState({
copy,
isDisable,
disableObjective,
+ isMaxSupervisionCountValid,
close,
updateDraft,
confirm,
+ formatScheduledAtInput,
};
}
diff --git a/packages/web/src/features/supervisor/actions/use-supervisor-actions.ts b/packages/web/src/features/supervisor/actions/use-supervisor-actions.ts
index 6a43a1090..9c0781604 100644
--- a/packages/web/src/features/supervisor/actions/use-supervisor-actions.ts
+++ b/packages/web/src/features/supervisor/actions/use-supervisor-actions.ts
@@ -1,9 +1,11 @@
-import type { Supervisor, SupervisorCycle, SupervisorState } from "@coder-studio/core";
+import type { SupervisorCycle, SupervisorState } from "@coder-studio/core";
import { useAtomValue, useSetAtom } from "jotai";
import { useCallback, useEffect, useState } from "react";
+import { localeAtom } from "../../../atoms/app-ui";
import { dispatchCommandAtom } from "../../../atoms/connection";
-import { useTranslation } from "../../../lib/i18n";
+import { formatDate, type LocaleCode, useTranslation } from "../../../lib/i18n";
import { supervisorCyclesAtom, supervisorDialogAtom, supervisorsAtom } from "../atoms";
+import { formatScheduledAtInput } from "./use-objective-dialog-state";
const STATE_CLASSES: Record
= {
inactive: "supervisor-state-inactive",
@@ -12,6 +14,7 @@ const STATE_CLASSES: Record = {
injecting: "supervisor-state-injecting",
paused: "supervisor-state-paused",
error: "supervisor-state-error",
+ stopped: "supervisor-state-idle",
};
interface UseSupervisorActionsArgs {
@@ -23,6 +26,7 @@ export function useSupervisorActions({ sessionId }: UseSupervisorActionsArgs) {
const cyclesBySupervisor = useAtomValue(supervisorCyclesAtom);
const dispatch = useAtomValue(dispatchCommandAtom);
const setDialog = useSetAtom(supervisorDialogAtom);
+ const locale = useAtomValue(localeAtom) as LocaleCode;
const t = useTranslation();
const supervisor = supervisors.get(sessionId);
const [actionError, setActionError] = useState(null);
@@ -45,6 +49,9 @@ export function useSupervisorActions({ sessionId }: UseSupervisorActionsArgs) {
draftObjective: supervisor?.objective ?? "",
draftEvaluatorProviderId:
(supervisor?.evaluatorProviderId as "claude" | "codex") ?? "claude",
+ draftEvaluatorModel: supervisor?.evaluatorModel ?? "",
+ draftMaxSupervisionCount: String(supervisor?.maxSupervisionCount ?? 0),
+ draftScheduledAt: formatScheduledAtInput(supervisor?.scheduledAt),
});
},
[sessionId, setDialog, supervisor]
@@ -102,12 +109,46 @@ export function useSupervisorActions({ sessionId }: UseSupervisorActionsArgs) {
? t("supervisor.cycle.no_guidance")
: latestCycle.status === "evaluating"
? t("supervisor.cycle.evaluating")
- : t("supervisor.cycle.waiting")))
+ : latestCycle.status === "cancelled"
+ ? t("supervisor.cycle.cancelled")
+ : t("supervisor.cycle.waiting")))
: null;
+ const stopReasonLabel = supervisor?.stopReason
+ ? t(`supervisor.stop_reason.${supervisor.stopReason}`)
+ : null;
+
+ const executionPolicyItems = supervisor
+ ? [
+ supervisor.evaluatorModel
+ ? {
+ key: "model",
+ label: t("supervisor.field.evaluator_model"),
+ value: supervisor.evaluatorModel,
+ }
+ : null,
+ {
+ key: "max-count",
+ label: t("supervisor.field.max_supervision_count"),
+ value:
+ supervisor.maxSupervisionCount > 0
+ ? String(supervisor.maxSupervisionCount)
+ : t("supervisor.meta.no_cap"),
+ },
+ supervisor.scheduledAt
+ ? {
+ key: "scheduled-at",
+ label: t("supervisor.field.scheduled_at"),
+ value: formatDate(supervisor.scheduledAt, locale),
+ }
+ : null,
+ ].filter((item): item is { key: string; label: string; value: string } => item !== null)
+ : [];
+
return {
actionError,
cycles,
+ executionPolicyItems,
handlePause,
handleResume,
handleTrigger,
@@ -115,6 +156,7 @@ export function useSupervisorActions({ sessionId }: UseSupervisorActionsArgs) {
latestCycle,
latestCycleText,
openDialog,
+ stopReasonLabel,
stateClass: supervisor ? STATE_CLASSES[supervisor.state] : STATE_CLASSES.inactive,
stateLabel: t(
`supervisor.state.${supervisor ? supervisor.state : ("inactive" as SupervisorState)}`
diff --git a/packages/web/src/features/supervisor/actions/use-supervisor.ts b/packages/web/src/features/supervisor/actions/use-supervisor.ts
index fbf88b0a0..d3a29102b 100644
--- a/packages/web/src/features/supervisor/actions/use-supervisor.ts
+++ b/packages/web/src/features/supervisor/actions/use-supervisor.ts
@@ -8,6 +8,7 @@ import {
supervisorHydratedAtomFamily,
supervisorsAtom,
} from "../atoms";
+import { formatScheduledAtInput } from "./use-objective-dialog-state";
const EMPTY_SESSION_ID = "__supervisor-empty__";
@@ -81,6 +82,9 @@ export function useSupervisor(session: Session | null | undefined) {
draftObjective: supervisor?.objective ?? "",
draftEvaluatorProviderId:
(supervisor?.evaluatorProviderId as "claude" | "codex") ?? "claude",
+ draftEvaluatorModel: supervisor?.evaluatorModel ?? "",
+ draftMaxSupervisionCount: String(supervisor?.maxSupervisionCount ?? 0),
+ draftScheduledAt: formatScheduledAtInput(supervisor?.scheduledAt),
});
},
[sessionId, setDialog]
diff --git a/packages/web/src/features/supervisor/atoms.ts b/packages/web/src/features/supervisor/atoms.ts
index bfce5804d..6b6f30b16 100644
--- a/packages/web/src/features/supervisor/atoms.ts
+++ b/packages/web/src/features/supervisor/atoms.ts
@@ -22,12 +22,18 @@ export const supervisorDialogAtom = atom<{
mode: "enable" | "edit" | "disable";
draftObjective: string;
draftEvaluatorProviderId: "claude" | "codex";
+ draftEvaluatorModel: string;
+ draftMaxSupervisionCount: string;
+ draftScheduledAt: string;
}>({
open: false,
sessionId: null,
mode: "enable",
draftObjective: "",
draftEvaluatorProviderId: "claude",
+ draftEvaluatorModel: "",
+ draftMaxSupervisionCount: "0",
+ draftScheduledAt: "",
});
// Derived atom for getting supervisor by session
diff --git a/packages/web/src/features/supervisor/components/objective-dialog.test.tsx b/packages/web/src/features/supervisor/components/objective-dialog.test.tsx
index b404f3b56..8146e345f 100644
--- a/packages/web/src/features/supervisor/components/objective-dialog.test.tsx
+++ b/packages/web/src/features/supervisor/components/objective-dialog.test.tsx
@@ -20,6 +20,43 @@ afterEach(() => {
});
describe("ObjectiveDialog", () => {
+ const createDialogState = (
+ overrides: Partial<{
+ open: boolean;
+ sessionId: string | null;
+ mode: "enable" | "edit" | "disable";
+ draftObjective: string;
+ draftEvaluatorProviderId: "claude" | "codex";
+ draftEvaluatorModel: string;
+ draftMaxSupervisionCount: string;
+ draftScheduledAt: string;
+ }> = {}
+ ) => ({
+ open: true,
+ sessionId: "sess-1",
+ mode: "enable" as const,
+ draftObjective: "",
+ draftEvaluatorProviderId: "claude" as const,
+ draftEvaluatorModel: "",
+ draftMaxSupervisionCount: "0",
+ draftScheduledAt: "",
+ ...overrides,
+ });
+
+ const createSupervisor = () => ({
+ id: "sup-1",
+ sessionId: "sess-1",
+ workspaceId: "ws-1",
+ state: "idle" as const,
+ objective: "Finish the server refactor",
+ evaluatorProviderId: "claude",
+ maxSupervisionCount: 0,
+ completedSupervisionCount: 0,
+ cycles: [],
+ createdAt: 1,
+ updatedAt: 1,
+ });
+
it("submits evaluatorProviderId during enable", async () => {
const user = userEvent.setup();
const sendCommand = vi.fn().mockResolvedValue(undefined);
@@ -27,13 +64,13 @@ describe("ObjectiveDialog", () => {
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand } as never);
- store.set(supervisorDialogAtom, {
- open: true,
- sessionId: "sess-1",
- mode: "enable",
- draftObjective: "Finish the server refactor",
- draftEvaluatorProviderId: "codex",
- });
+ store.set(
+ supervisorDialogAtom,
+ createDialogState({
+ draftObjective: "Finish the server refactor",
+ draftEvaluatorProviderId: "codex",
+ })
+ );
store.set(supervisorsAtom, new Map());
render(
@@ -54,24 +91,53 @@ describe("ObjectiveDialog", () => {
workspaceId: "ws-1",
objective: "Finish the server refactor",
evaluatorProviderId: "claude",
+ evaluatorModel: undefined,
+ maxSupervisionCount: 0,
+ scheduledAt: undefined,
},
undefined
);
});
});
+ it("blocks submit when maxSupervisionCount is invalid instead of coercing to unlimited", async () => {
+ const user = userEvent.setup();
+ const sendCommand = vi.fn().mockResolvedValue(undefined);
+ const store = createStore();
+ window.localStorage.setItem("ui.locale", JSON.stringify("en"));
+ store.set(localeAtom, "en");
+ store.set(wsClientAtom, { sendCommand } as never);
+ store.set(
+ supervisorDialogAtom,
+ createDialogState({
+ draftObjective: "Finish the server refactor",
+ draftMaxSupervisionCount: "-1",
+ })
+ );
+ store.set(supervisorsAtom, new Map());
+
+ render(
+
+
+
+ );
+
+ await user.click(screen.getByRole("button", { name: "Enable" }));
+
+ expect(sendCommand).not.toHaveBeenCalled();
+ });
+
it("renders the evaluator field through the shared select trigger with label and helper wiring", () => {
const store = createStore();
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
- store.set(supervisorDialogAtom, {
- open: true,
- sessionId: "sess-1",
- mode: "enable",
- draftObjective: "Ship phase 4B1",
- draftEvaluatorProviderId: "claude",
- });
+ store.set(
+ supervisorDialogAtom,
+ createDialogState({
+ draftObjective: "Ship phase 4B1",
+ })
+ );
store.set(supervisorsAtom, new Map());
render(
@@ -94,13 +160,12 @@ describe("ObjectiveDialog", () => {
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
- store.set(supervisorDialogAtom, {
- open: true,
- sessionId: "sess-1",
- mode: "enable",
- draftObjective: "Ship phase 4B1",
- draftEvaluatorProviderId: "claude",
- });
+ store.set(
+ supervisorDialogAtom,
+ createDialogState({
+ draftObjective: "Ship phase 4B1",
+ })
+ );
store.set(supervisorsAtom, new Map());
render(
@@ -117,32 +182,13 @@ describe("ObjectiveDialog", () => {
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
- store.set(supervisorDialogAtom, {
- open: true,
- sessionId: "sess-1",
- mode: "disable",
- draftObjective: "",
- draftEvaluatorProviderId: "claude",
- });
store.set(
- supervisorsAtom,
- new Map([
- [
- "sess-1",
- {
- id: "sup-1",
- sessionId: "sess-1",
- workspaceId: "ws-1",
- state: "idle",
- objective: "Finish the server refactor",
- evaluatorProviderId: "claude",
- cycles: [],
- createdAt: 1,
- updatedAt: 1,
- },
- ],
- ])
+ supervisorDialogAtom,
+ createDialogState({
+ mode: "disable",
+ })
);
+ store.set(supervisorsAtom, new Map([["sess-1", createSupervisor()]]));
render(
@@ -160,13 +206,12 @@ describe("ObjectiveDialog", () => {
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
- store.set(supervisorDialogAtom, {
- open: true,
- sessionId: "sess-1",
- mode: "disable",
- draftObjective: "",
- draftEvaluatorProviderId: "claude",
- });
+ store.set(
+ supervisorDialogAtom,
+ createDialogState({
+ mode: "disable",
+ })
+ );
store.set(supervisorsAtom, new Map());
render(
@@ -184,13 +229,12 @@ describe("ObjectiveDialog", () => {
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
- store.set(supervisorDialogAtom, {
- open: true,
- sessionId: "sess-1",
- mode: "disable",
- draftObjective: "",
- draftEvaluatorProviderId: "claude",
- });
+ store.set(
+ supervisorDialogAtom,
+ createDialogState({
+ mode: "disable",
+ })
+ );
store.set(supervisorsAtom, new Map());
render(
@@ -207,13 +251,12 @@ describe("ObjectiveDialog", () => {
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
- store.set(supervisorDialogAtom, {
- open: true,
- sessionId: "sess-1",
- mode: "enable",
- draftObjective: "Ship phase 4B1",
- draftEvaluatorProviderId: "claude",
- });
+ store.set(
+ supervisorDialogAtom,
+ createDialogState({
+ draftObjective: "Ship phase 4B1",
+ })
+ );
store.set(supervisorsAtom, new Map());
render(
@@ -230,13 +273,12 @@ describe("ObjectiveDialog", () => {
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
- store.set(supervisorDialogAtom, {
- open: true,
- sessionId: "sess-1",
- mode: "enable",
- draftObjective: "Ship phase 4B1",
- draftEvaluatorProviderId: "claude",
- });
+ store.set(
+ supervisorDialogAtom,
+ createDialogState({
+ draftObjective: "Ship phase 4B1",
+ })
+ );
store.set(supervisorsAtom, new Map());
render(
@@ -254,13 +296,12 @@ describe("ObjectiveDialog", () => {
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
- store.set(supervisorDialogAtom, {
- open: true,
- sessionId: "sess-1",
- mode: "enable",
- draftObjective: "Ship phase 4B1",
- draftEvaluatorProviderId: "claude",
- });
+ store.set(
+ supervisorDialogAtom,
+ createDialogState({
+ draftObjective: "Ship phase 4B1",
+ })
+ );
store.set(supervisorsAtom, new Map());
const { container } = render(
diff --git a/packages/web/src/features/supervisor/components/supervisor-card.test.tsx b/packages/web/src/features/supervisor/components/supervisor-card.test.tsx
index 1f0947573..1cd364614 100644
--- a/packages/web/src/features/supervisor/components/supervisor-card.test.tsx
+++ b/packages/web/src/features/supervisor/components/supervisor-card.test.tsx
@@ -4,6 +4,7 @@ import { createStore, Provider } from "jotai";
import { describe, expect, it, vi } from "vitest";
import { localeAtom } from "../../../atoms/app-ui";
import { wsClientAtom } from "../../../atoms/connection";
+import { formatDate } from "../../../lib/i18n";
import { supervisorCyclesAtom, supervisorsAtom } from "../atoms";
import { SupervisorCard } from "../views/shared/supervisor-card";
@@ -15,6 +16,8 @@ describe("SupervisorCard", () => {
state: "idle",
objective: "Finish the server refactor",
evaluatorProviderId: "codex",
+ maxSupervisionCount: 0,
+ completedSupervisionCount: 0,
cycles: [],
createdAt: 1,
updatedAt: 1,
@@ -101,7 +104,7 @@ describe("SupervisorCard", () => {
);
- expect(screen.getByRole("button", { name: "Edit Objective" })).toHaveClass(
+ expect(screen.getByRole("button", { name: "Edit Supervisor" })).toHaveClass(
"btn",
"btn-ghost",
"btn-sm",
@@ -119,7 +122,7 @@ describe("SupervisorCard", () => {
"btn-sm",
"supervisor-icon-btn"
);
- expect(screen.getByRole("button", { name: "Disable Supervisor" })).toHaveClass(
+ expect(screen.getByRole("button", { name: "Disable" })).toHaveClass(
"btn",
"btn-ghost",
"btn-sm",
@@ -182,4 +185,125 @@ describe("SupervisorCard", () => {
expect(screen.queryByText("65%")).not.toBeInTheDocument();
expect(document.querySelector(".supervisor-progress-track")).not.toBeInTheDocument();
});
+
+ it("keeps pause available while the supervisor is evaluating", () => {
+ const store = createStore();
+ window.localStorage.setItem("ui.locale", JSON.stringify("en"));
+ store.set(localeAtom, "en");
+ store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
+ store.set(
+ supervisorsAtom,
+ new Map([["sess-1", { ...createSupervisor(), state: "evaluating" }]])
+ );
+ store.set(supervisorCyclesAtom, new Map());
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole("button", { name: "Pause" })).not.toBeDisabled();
+ });
+
+ it("renders configured execution policy metadata", () => {
+ const store = createStore();
+ const scheduledAt = Date.UTC(2026, 4, 11, 3, 0);
+ window.localStorage.setItem("ui.locale", JSON.stringify("en"));
+ store.set(localeAtom, "en");
+ store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
+ store.set(
+ supervisorsAtom,
+ new Map([
+ [
+ "sess-1",
+ {
+ ...createSupervisor(),
+ evaluatorModel: "o3",
+ maxSupervisionCount: 5,
+ scheduledAt,
+ },
+ ],
+ ])
+ );
+ store.set(supervisorCyclesAtom, new Map());
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("Evaluator Model")).toBeInTheDocument();
+ expect(screen.getByText("o3")).toBeInTheDocument();
+ expect(screen.getByText("Max Supervision Count")).toBeInTheDocument();
+ expect(screen.getByText("5")).toBeInTheDocument();
+ expect(screen.getByText("Scheduled Run Time")).toBeInTheDocument();
+ expect(screen.getByText(formatDate(scheduledAt, "en"))).toBeInTheDocument();
+ });
+
+ it("shows a no-cap max supervision count when the limit is disabled", () => {
+ const store = createStore();
+ window.localStorage.setItem("ui.locale", JSON.stringify("en"));
+ store.set(localeAtom, "en");
+ store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
+ store.set(supervisorsAtom, new Map([["sess-1", createSupervisor()]]));
+ store.set(supervisorCyclesAtom, new Map());
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("Max Supervision Count")).toBeInTheDocument();
+ expect(screen.getByText("No cap")).toBeInTheDocument();
+ });
+
+ it("renders stopped reason and scheduled cancelled cycle details", () => {
+ const store = createStore();
+ window.localStorage.setItem("ui.locale", JSON.stringify("en"));
+ store.set(localeAtom, "en");
+ store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
+ store.set(
+ supervisorsAtom,
+ new Map([
+ [
+ "sess-1",
+ {
+ ...createSupervisor(),
+ state: "stopped",
+ stopReason: "objective_complete",
+ },
+ ],
+ ])
+ );
+ store.set(
+ supervisorCyclesAtom,
+ new Map([
+ [
+ "sup-1",
+ [
+ createCycle({
+ status: "cancelled",
+ trigger: "scheduled",
+ completedAt: 3,
+ }),
+ ],
+ ],
+ ])
+ );
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("SCHEDULED")).toBeInTheDocument();
+ expect(screen.getByText("Cancelled")).toBeInTheDocument();
+ expect(
+ screen.getByText("Objective complete. Supervisor stopped automatically.")
+ ).toBeInTheDocument();
+ });
});
diff --git a/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-badge.tsx b/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-badge.tsx
index e1ab1f5af..c911b9d85 100644
--- a/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-badge.tsx
+++ b/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-badge.tsx
@@ -3,7 +3,7 @@ import { useAtomValue } from "jotai";
import { Eye } from "lucide-react";
import { useMemo } from "react";
import { useTranslation } from "../../../../lib/i18n";
-import { supervisorCyclesAtom, supervisorsAtom } from "../../atoms";
+import { supervisorsAtom } from "../../atoms";
interface MobileSupervisorBadgeProps {
sessionId: string | null;
@@ -12,7 +12,6 @@ interface MobileSupervisorBadgeProps {
export function MobileSupervisorBadge({ sessionId, onOpen }: MobileSupervisorBadgeProps) {
const supervisors = useAtomValue(supervisorsAtom);
- const cyclesBySupervisor = useAtomValue(supervisorCyclesAtom);
const t = useTranslation();
const copy = useMemo(() => {
@@ -28,19 +27,11 @@ export function MobileSupervisorBadge({ sessionId, onOpen }: MobileSupervisorBad
};
}
- const cycles = cyclesBySupervisor.get(supervisor.id) ?? supervisor.cycles ?? [];
- const latestCycle = [...cycles].sort(
- (left, right) => (right.completedAt ?? right.createdAt) - (left.completedAt ?? left.createdAt)
- )[0];
-
return {
state: supervisor.state,
- label:
- latestCycle?.result ??
- latestCycle?.errorReason ??
- (cycles.length > 0 ? `cycle ${cycles.length}` : supervisor.objective),
+ label: t("supervisor.title"),
};
- }, [cyclesBySupervisor, sessionId, supervisors, t]);
+ }, [sessionId, supervisors, t]);
if (!copy) {
return null;
diff --git a/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-sheet.test.tsx b/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-sheet.test.tsx
index 585a1541c..758be6b60 100644
--- a/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-sheet.test.tsx
+++ b/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-sheet.test.tsx
@@ -24,6 +24,43 @@ function setMatchMediaMock(predicate: (query: string) => boolean) {
describe("MobileSupervisorSheet", () => {
let originalMatchMedia: typeof window.matchMedia;
+ const createDialogState = (
+ overrides: Partial<{
+ open: boolean;
+ sessionId: string | null;
+ mode: "enable" | "edit" | "disable";
+ draftObjective: string;
+ draftEvaluatorProviderId: "claude" | "codex";
+ draftEvaluatorModel: string;
+ draftMaxSupervisionCount: string;
+ draftScheduledAt: string;
+ }> = {}
+ ) => ({
+ open: false,
+ sessionId: null,
+ mode: "enable" as const,
+ draftObjective: "",
+ draftEvaluatorProviderId: "claude" as const,
+ draftEvaluatorModel: "",
+ draftMaxSupervisionCount: "0",
+ draftScheduledAt: "",
+ ...overrides,
+ });
+
+ const createSupervisor = () => ({
+ id: "sup-1",
+ sessionId: "sess-1",
+ workspaceId: "ws-1",
+ state: "idle" as const,
+ objective: "Reduce mobile regression bugs",
+ evaluatorProviderId: "claude",
+ maxSupervisionCount: 0,
+ completedSupervisionCount: 0,
+ cycles: [],
+ createdAt: 1,
+ updatedAt: 1,
+ });
+
beforeEach(() => {
originalMatchMedia = window.matchMedia;
setMatchMediaMock(
@@ -41,25 +78,7 @@ describe("MobileSupervisorSheet", () => {
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
- store.set(
- supervisorsAtom,
- new Map([
- [
- "sess-1",
- {
- id: "sup-1",
- sessionId: "sess-1",
- workspaceId: "ws-1",
- state: "idle",
- objective: "Reduce mobile regression bugs",
- evaluatorProviderId: "claude",
- cycles: [],
- createdAt: 1,
- updatedAt: 1,
- },
- ],
- ])
- );
+ store.set(supervisorsAtom, new Map([["sess-1", createSupervisor()]]));
render(
@@ -72,15 +91,18 @@ describe("MobileSupervisorSheet", () => {
expect(screen.getByText("Reduce mobile regression bugs")).toBeInTheDocument();
expect(
- within(rootActions as HTMLElement).getByRole("button", { name: "Edit Objective" })
+ within(rootActions as HTMLElement).getByRole("button", { name: "Edit Supervisor" })
).toBeInTheDocument();
expect(
- within(rootActions as HTMLElement).getByRole("button", { name: "Disable Supervisor" })
+ within(rootActions as HTMLElement).getByRole("button", { name: "Disable" })
).toBeInTheDocument();
expect(screen.queryByText("Supervisor is not enabled")).not.toBeInTheDocument();
+ expect(
+ document.querySelector(".mobile-supervisor-sheet.mobile-sheet--fullscreen")
+ ).not.toBeNull();
});
- it("opens the enable flow inside the same sheet without rendering a second overlay", async () => {
+ it("renders the enable form directly when supervisor is not enabled", async () => {
const sendCommand = vi.fn().mockResolvedValue({ id: "sup-1" });
const store = createStore();
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
@@ -94,19 +116,16 @@ describe("MobileSupervisorSheet", () => {
);
- const emptyState = screen
- .getByText("Supervisor is not enabled")
- .closest(".mobile-supervisor-sheet__empty");
-
- expect(emptyState).not.toBeNull();
- expect(emptyState).toHaveTextContent("Supervisor");
- expect(emptyState).toHaveTextContent("Supervisor is not enabled");
-
- fireEvent.click(screen.getByRole("button", { name: "Enable Objective" }));
-
expect(screen.getByLabelText("Objective")).toBeInTheDocument();
+ expect(
+ screen.getByRole("heading", { name: "Enable Supervisor", level: 2 })
+ ).toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: "Enable Objective" })).not.toBeInTheDocument();
expect(document.querySelectorAll(".mobile-sheet-layer")).toHaveLength(1);
expect(document.querySelector(".modal-overlay")).toBeNull();
+ expect(
+ document.querySelector(".mobile-supervisor-sheet.mobile-sheet--fullscreen")
+ ).not.toBeNull();
fireEvent.change(screen.getByLabelText("Objective"), {
target: { value: "Reduce mobile regression bugs" },
@@ -121,25 +140,22 @@ describe("MobileSupervisorSheet", () => {
workspaceId: "ws-1",
objective: "Reduce mobile regression bugs",
evaluatorProviderId: "claude",
+ evaluatorModel: undefined,
+ maxSupervisionCount: 0,
+ scheduledAt: undefined,
},
undefined
);
});
});
- it("returns from detail view to the supervisor root when tapping back", () => {
+ it("returns from edit detail view to the supervisor root when tapping back", () => {
const store = createStore();
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
store.set(localeAtom, "en");
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
- store.set(supervisorsAtom, new Map());
- store.set(supervisorDialogAtom, {
- open: false,
- sessionId: null,
- mode: "enable",
- draftObjective: "",
- draftEvaluatorProviderId: "claude",
- });
+ store.set(supervisorsAtom, new Map([["sess-1", createSupervisor()]]));
+ store.set(supervisorDialogAtom, createDialogState());
render(
@@ -147,15 +163,19 @@ describe("MobileSupervisorSheet", () => {
);
- fireEvent.click(screen.getByRole("button", { name: "Enable Objective" }));
+ const rootActions = document.querySelector(".mobile-supervisor-sheet__actions");
+ expect(rootActions).not.toBeNull();
+
+ fireEvent.click(
+ within(rootActions as HTMLElement).getByRole("button", { name: "Edit Supervisor" })
+ );
fireEvent.click(screen.getByRole("button", { name: "Back" }));
- expect(screen.getByText("Supervisor is not enabled")).toBeInTheDocument();
+ expect(screen.getByText("Reduce mobile regression bugs")).toBeInTheDocument();
expect(screen.queryByLabelText("Objective")).not.toBeInTheDocument();
});
it("renders mobile text actions with shared button compatibility classes", async () => {
- const user = userEvent.setup();
const store = createStore();
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
@@ -169,13 +189,6 @@ describe("MobileSupervisorSheet", () => {
);
- expect(screen.getByRole("button", { name: "Enable Objective" })).toHaveClass(
- "btn",
- "btn-primary"
- );
-
- await user.click(screen.getByRole("button", { name: "Enable Objective" }));
-
expect(screen.getByRole("button", { name: "Cancel" })).toHaveClass("btn", "btn-secondary");
expect(screen.getByRole("button", { name: "Enable" })).toHaveClass("btn", "btn-primary");
});
@@ -195,7 +208,6 @@ describe("MobileSupervisorSheet", () => {
);
- await user.click(screen.getByRole("button", { name: "Enable Objective" }));
await user.click(screen.getByRole("button", { name: "Evaluator Claude" }));
expect(document.querySelector(".mobile-inline-sheet .page-header__title")).toHaveTextContent(
@@ -210,7 +222,6 @@ describe("MobileSupervisorSheet", () => {
});
it("keeps the migrated evaluator trigger compatibility classes in the mobile detail sheet", async () => {
- const user = userEvent.setup();
const store = createStore();
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
@@ -224,8 +235,6 @@ describe("MobileSupervisorSheet", () => {
);
- await user.click(screen.getByRole("button", { name: "Enable Objective" }));
-
const trigger = screen.getByRole("button", { name: "Evaluator Claude" });
expect(trigger).toHaveClass("input", "mobile-select-trigger");
expect(trigger.querySelector(".mobile-select-trigger__value")).not.toBeNull();
diff --git a/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-sheet.tsx b/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-sheet.tsx
index 749663ce4..a1e09c9c0 100644
--- a/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-sheet.tsx
+++ b/packages/web/src/features/supervisor/views/mobile/mobile-supervisor-sheet.tsx
@@ -1,8 +1,9 @@
import { useSetAtom } from "jotai";
import { useEffect, useState } from "react";
-import { Button, EmptyState, Sheet } from "../../../../components/ui";
+import { Button, Sheet } from "../../../../components/ui";
import { useTranslation } from "../../../../lib/i18n";
import {
+ formatScheduledAtInput,
type ObjectiveDialogEvaluatorProviderId,
type ObjectiveDialogMode,
useObjectiveDialogState,
@@ -20,23 +21,6 @@ interface MobileSupervisorSheetProps {
onClose: () => void;
}
-const mobileSupervisorEmptyStateStyle = {
- minHeight: 0,
- padding: "var(--sp-4)",
- gap: "var(--sp-2)",
- alignItems: "stretch",
- textAlign: "left",
-};
-
-const mobileSupervisorEmptySlotStyle = {
- width: "auto",
- maxWidth: "none",
- color: "inherit",
- fontSize: "inherit",
- fontWeight: "inherit",
- lineHeight: "inherit",
-};
-
export function MobileSupervisorSheet({
sessionId,
workspaceId,
@@ -52,11 +36,50 @@ export function MobileSupervisorSheet({
copy,
isDisable,
disableObjective,
+ isMaxSupervisionCountValid,
close,
updateDraft,
confirm,
} = useObjectiveDialogState({ workspaceId, sessionId });
+ useEffect(() => {
+ if (supervisor || detailMode) {
+ return;
+ }
+
+ setDialog((current) => {
+ if (current.sessionId === sessionId && current.mode === "enable" && !current.open) {
+ return current;
+ }
+
+ return {
+ open: false,
+ sessionId,
+ mode: "enable",
+ draftObjective:
+ current.sessionId === sessionId && current.mode === "enable"
+ ? current.draftObjective
+ : "",
+ draftEvaluatorProviderId:
+ current.sessionId === sessionId && current.mode === "enable"
+ ? current.draftEvaluatorProviderId
+ : "claude",
+ draftEvaluatorModel:
+ current.sessionId === sessionId && current.mode === "enable"
+ ? current.draftEvaluatorModel
+ : "",
+ draftMaxSupervisionCount:
+ current.sessionId === sessionId && current.mode === "enable"
+ ? current.draftMaxSupervisionCount
+ : "0",
+ draftScheduledAt:
+ current.sessionId === sessionId && current.mode === "enable"
+ ? current.draftScheduledAt
+ : "",
+ };
+ });
+ }, [detailMode, sessionId, setDialog, supervisor]);
+
useEffect(() => {
if (!dialog.open || dialog.sessionId !== sessionId) {
setDetailMode(null);
@@ -74,10 +97,76 @@ export function MobileSupervisorSheet({
draftObjective: supervisor?.objective ?? "",
draftEvaluatorProviderId:
(supervisor?.evaluatorProviderId as ObjectiveDialogEvaluatorProviderId) ?? "claude",
+ draftEvaluatorModel: supervisor?.evaluatorModel ?? "",
+ draftMaxSupervisionCount: String(supervisor?.maxSupervisionCount ?? 0),
+ draftScheduledAt: formatScheduledAtInput(supervisor?.scheduledAt),
});
setDetailMode(nextMode);
};
+ const detailBody = (
+
+
+
+
+
+
+
{copy.title}
+
{copy.subtitle}
+
+
+
updateDraft({ draftObjective })}
+ onDraftEvaluatorProviderChange={(draftEvaluatorProviderId) =>
+ updateDraft({ draftEvaluatorProviderId })
+ }
+ onDraftEvaluatorModelChange={(draftEvaluatorModel) => updateDraft({ draftEvaluatorModel })}
+ onDraftMaxSupervisionCountChange={(draftMaxSupervisionCount) =>
+ updateDraft({ draftMaxSupervisionCount })
+ }
+ onDraftScheduledAtChange={(draftScheduledAt) => updateDraft({ draftScheduledAt })}
+ />
+
+ );
+
+ const detailFooter = (
+
+ {
+ close();
+ setDetailMode(null);
+ if (!supervisor) {
+ onClose();
+ }
+ }}
+ >
+ {t("action.cancel")}
+
+ {
+ void (async () => {
+ const ok = await confirm();
+ if (ok && !supervisor) {
+ onClose();
+ }
+ })();
+ }}
+ disabled={!isDisable && !dialog.draftObjective.trim()}
+ >
+ {copy.confirm}
+
+
+ );
+
if (detailMode) {
return (
-
-
-
-
-
-
{copy.title}
-
{copy.subtitle}
-
-
- updateDraft({ draftObjective })}
- onDraftEvaluatorProviderChange={(draftEvaluatorProviderId) =>
- updateDraft({ draftEvaluatorProviderId })
- }
- />
-
- }
- footer={
-