Files
paseo/packages/app/src/components/schedules/schedule-form-sheet.tsx
T
35f985baff Switch reasoning, mode, plan, and fast from the Command Center (#2274)
* Switch reasoning, mode, plan, and fast from the Command Center

Extend the Command Center (Cmd-K) with selectors for a focused agent's
Thinking (reasoning effort), access Mode, Plan mode, and Fast mode,
mirroring the model switcher. Typing surfaces tiered "<Setting> › <Value>"
breadcrumb rows with icons and a check on the current value, for both a
running agent and a new draft tab. Rows only appear while typing, so the
default palette is unchanged.

- Thinking and Mode are multi-option selects; Plan mode and Fast mode are
  On/Off toggles.
- Plan mode is dual-natured: a permission mode for Claude/Copilot/OpenCode
  (excluded from the Mode list, given its own toggle) and the plan_mode
  feature for Codex. Fast mode drives the fast_mode feature (model-gated).
- All logic lives in command-center/agent-setting-contributions.ts as pure,
  unit-tested builders, with a small icon helper and a hook that registers
  the running-agent selectors; the draft registers from its composer tab.
  No changes to the command palette component and no protocol changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Unify Command Center agent controls

* refactor: clarify agent control ownership

* refactor(app): tighten command center agent controls

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-31 17:37:04 +02:00

1087 lines
30 KiB
TypeScript

import equal from "fast-deep-equal";
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
type ReactElement,
type ReactNode,
} from "react";
import { Text, View } from "react-native";
import { Brain, Folder, GitBranch } from "lucide-react-native";
import { StyleSheet } from "react-native-unistyles";
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
import type { ScheduleCadence, ScheduleSummary } from "@getpaseo/protocol/schedule/types";
import { useStoreWithEqualityFn } from "zustand/traditional";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
import { ComboboxItem } from "@/components/ui/combobox";
import { Button } from "@/components/ui/button";
import { CombinedModelSelector } from "@/components/combined-model-selector";
import { useIsCompactFormFactor } from "@/constants/layout";
import { HostStatusDotSlot } from "@/components/hosts/host-picker";
import { createControlGeometry, type FieldControlSize } from "@/components/ui/control-geometry";
import { Field, FormTextInput } from "@/components/ui/form-field";
import { Switch } from "@/components/ui/switch";
import { getProviderIcon } from "@/components/provider-icons";
import { CadenceEditor } from "@/components/schedules/cadence-editor";
import {
SelectField,
SelectFieldTrigger,
type SelectFieldDisplay,
type SelectFieldOption,
type SelectFieldRenderOptionInput,
} from "@/components/ui/select-field";
import { formatThinkingOptionLabel } from "@/agent-controls/labels";
import {
mergeProviderPreferences,
useFormPreferences,
type FormPreferences,
} from "@/hooks/use-form-preferences";
import { useScheduleMutations } from "@/hooks/use-schedule-mutations";
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
import { useProjects } from "@/hooks/use-projects";
import { useHosts } from "@/runtime/host-runtime";
import { useSessionStore } from "@/stores/session-store";
import { buildScheduleProjectTargets } from "@/schedules/schedule-project-targets";
import { useScheduleFormModel } from "@/schedules/use-schedule-form-model";
import { useScheduleFormProviderSnapshot } from "@/schedules/use-schedule-form-provider-snapshot";
import type {
ScheduleFormDisplay,
ScheduleFormHost,
ScheduleFormModel,
ScheduleFormSnapshot,
ScheduleFormState,
} from "@/schedules/schedule-form-model";
import { validateCron } from "@/utils/schedule-format";
import { toErrorMessage } from "@/utils/error-messages";
import { getDeviceTimeZone } from "@/utils/device-timezone";
export interface ScheduleFormSheetProps {
serverId?: string;
visible: boolean;
onClose: () => void;
mode: "create" | "edit";
schedule?: ScheduleSummary;
}
function parseMaxRuns(raw: string): number | null {
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
function requireCronCadence(
cadence: Extract<ScheduleCadence, { type: "cron" }> | undefined,
): Extract<ScheduleCadence, { type: "cron" }> {
if (!cadence) {
throw new Error("Choose a cron cadence before creating this schedule");
}
return cadence;
}
function resolveCreateServerId(input: {
mode: "create" | "edit";
serverId: string | null | undefined;
hosts: readonly ScheduleFormHost[];
}): string | null {
if (input.mode === "edit") {
return input.serverId ?? null;
}
if (input.serverId !== undefined) {
return input.serverId;
}
if (input.hosts.length === 1) {
return input.hosts[0]?.serverId ?? null;
}
return null;
}
function buildScheduleHostOptionTestId(serverId: string): string {
return `schedule-host-option-${serverId}`;
}
function buildThinkingOptionTestId(optionId: string): string {
return `schedule-thinking-option-${optionId}`;
}
function openKey(props: ScheduleFormSheetProps): string {
if (props.mode === "edit") {
return `edit:${props.serverId ?? ""}:${props.schedule?.id ?? ""}`;
}
return `create:${props.serverId ?? ""}`;
}
function selectScheduleHosts(
hosts: readonly { serverId: string; label: string }[],
): (state: ReturnType<typeof useSessionStore.getState>) => ScheduleFormHost[] {
return (state) =>
hosts.map((host) => ({
serverId: host.serverId,
label: host.label,
supportsWorkspaceMultiplicity:
state.sessions[host.serverId]?.serverInfo?.features?.workspaceMultiplicity === true,
}));
}
function buildSnapshot(input: {
mode: "create" | "edit";
serverId: string | undefined;
schedule: ScheduleSummary | undefined;
hosts: readonly ScheduleFormHost[];
projectTargets: ReturnType<typeof buildScheduleProjectTargets>;
preferences: FormPreferences;
timezone: string;
}): ScheduleFormSnapshot {
const schedule = input.schedule
? { ...input.schedule, serverId: input.serverId, serverName: undefined }
: undefined;
return {
mode: input.mode,
schedule,
hosts: input.hosts,
defaults: {
serverId: resolveCreateServerId({
mode: input.mode,
serverId: input.serverId,
hosts: input.hosts,
}),
projectTargets: input.projectTargets,
preferences: input.preferences,
timezone: input.timezone,
},
};
}
function updateSelectionPreferences(input: {
preferences: FormPreferences;
provider: AgentProvider;
model: string;
mode: string;
thinkingOptionId: string;
isolation: "local" | "worktree";
}): FormPreferences {
const model = input.model.trim();
const mode = input.mode.trim();
const thinkingOptionId = input.thinkingOptionId.trim();
return {
...mergeProviderPreferences({
preferences: input.preferences,
provider: input.provider,
updates: {
model: model || undefined,
mode: mode || undefined,
...(model && thinkingOptionId ? { thinkingByModel: { [model]: thinkingOptionId } } : {}),
},
}),
isolation: input.isolation,
};
}
export function ScheduleFormSheet(props: ScheduleFormSheetProps): ReactElement | null {
const [renderedProps, setRenderedProps] = useState<ScheduleFormSheetProps | null>(() =>
props.visible ? props : null,
);
const [sheetVisible, setSheetVisible] = useState(props.visible);
const livePropsRef = useRef(props);
const closeRequestedRef = useRef(false);
livePropsRef.current = props;
useEffect(() => {
if (props.visible) {
if (closeRequestedRef.current) {
return;
}
setRenderedProps(props);
setSheetVisible(true);
return;
}
if (renderedProps) {
setSheetVisible(false);
}
}, [props, renderedProps]);
const requestClose = useCallback(() => {
closeRequestedRef.current = true;
setSheetVisible(false);
}, []);
const handleDismiss = useCallback(() => {
const dismissedProps = livePropsRef.current;
closeRequestedRef.current = false;
setRenderedProps(null);
setSheetVisible(false);
if (dismissedProps.visible) {
dismissedProps.onClose();
}
}, []);
if (!renderedProps) {
return null;
}
return (
<OpenScheduleFormSheet
key={openKey(renderedProps)}
{...renderedProps}
visible={sheetVisible}
onClose={requestClose}
onDismiss={handleDismiss}
/>
);
}
function OpenScheduleFormSheet({
serverId,
visible,
onClose,
onDismiss,
mode,
schedule,
}: ScheduleFormSheetProps & { onDismiss: () => void }): ReactElement {
const controlSize: FieldControlSize = useIsCompactFormFactor() ? "md" : "sm";
const { projects } = useProjects();
const hostProfiles = useHosts();
const hosts = useStoreWithEqualityFn(
useSessionStore,
useMemo(() => selectScheduleHosts(hostProfiles), [hostProfiles]),
equal,
);
const { preferences, updatePreferences } = useFormPreferences();
const projectTargets = useMemo(() => buildScheduleProjectTargets(projects), [projects]);
const timezone = useMemo(getDeviceTimeZone, []);
const snapshot = useMemo(
() =>
buildSnapshot({
mode,
serverId,
schedule,
hosts,
projectTargets,
preferences,
timezone,
}),
[hosts, mode, preferences, projectTargets, schedule, serverId, timezone],
);
const model = useScheduleFormModel(snapshot);
const state = useSyncExternalStore(model.subscribe, model.getState, model.getState);
const providerSnapshot = useScheduleFormProviderSnapshot(model, state);
const { agents } = useAggregatedAgents({ includeArchived: true });
const mutationServerId = state.selectedServerId ?? serverId ?? "";
const { createSchedule, updateSchedule, isCreating, isUpdating } = useScheduleMutations({
serverId: mutationServerId,
});
const isSubmitting = isCreating || isUpdating;
const cadenceError =
state.cadence.type === "cron" ? validateCron(state.cadence.expression) : null;
const canSubmit = state.canSubmit && cadenceError === null && !isSubmitting;
const agentTargetLabel = useMemo(() => {
if (!schedule || schedule.target.type !== "agent") {
return null;
}
const { agentId } = schedule.target;
const agent = agents.find(
(entry) => entry.serverId === (state.selectedServerId ?? serverId) && entry.id === agentId,
);
if (!agent) {
return "Agent unavailable";
}
return agent.title?.trim() || "Untitled agent";
}, [agents, schedule, serverId, state.selectedServerId]);
const persistPreferences = useCallback(async () => {
const provider = state.selectedProvider;
if (!provider) {
return;
}
await updatePreferences((current) =>
updateSelectionPreferences({
preferences: current,
provider,
model: state.selectedModel,
mode: state.selectedMode,
thinkingOptionId: state.selectedThinkingOptionId,
isolation: state.isolation,
}),
);
}, [
state.isolation,
state.selectedMode,
state.selectedModel,
state.selectedProvider,
state.selectedThinkingOptionId,
updatePreferences,
]);
const submitAgentTarget = useCallback(async (): Promise<boolean> => {
if (!schedule || !state.submitCadence) {
return false;
}
await updateSchedule({
id: schedule.id,
cadence: state.submitCadence,
});
return true;
}, [schedule, state.submitCadence, updateSchedule]);
const submitNewAgent = useCallback(async (): Promise<boolean> => {
const provider = state.selectedProvider;
const cwd = state.workingDir.trim();
if (!provider || !cwd) {
return false;
}
await persistPreferences();
const maxRuns = parseMaxRuns(state.maxRuns);
if (mode === "edit" && schedule) {
await updateSchedule({
id: schedule.id,
name: state.name.trim() || null,
prompt: state.prompt.trim(),
...(state.submitCadence ? { cadence: state.submitCadence } : {}),
newAgentConfig: {
provider,
model: state.selectedModel || null,
modeId: state.selectedMode || null,
thinkingOptionId: state.selectedThinkingOptionId || null,
cwd,
...(state.submitArchiveOnFinish !== undefined
? { archiveOnFinish: state.submitArchiveOnFinish }
: {}),
...(state.submitIsolation !== undefined ? { isolation: state.submitIsolation } : {}),
},
maxRuns,
});
return true;
}
await createSchedule({
prompt: state.prompt.trim(),
name: state.name.trim() || undefined,
cadence: requireCronCadence(state.submitCadence),
target: {
type: "new-agent",
config: {
provider,
cwd,
model: state.selectedModel || undefined,
modeId: state.selectedMode || undefined,
thinkingOptionId: state.selectedThinkingOptionId || undefined,
...(state.submitArchiveOnFinish !== undefined
? { archiveOnFinish: state.submitArchiveOnFinish }
: {}),
...(state.submitIsolation !== undefined ? { isolation: state.submitIsolation } : {}),
title: state.name.trim() || undefined,
},
},
...(maxRuns != null ? { maxRuns } : {}),
});
return true;
}, [createSchedule, mode, persistPreferences, schedule, state, updateSchedule]);
const handleSubmit = useCallback(async () => {
if (!canSubmit) {
return;
}
model.setSubmitError(null);
try {
const submitted =
state.targetKind === "agent" ? await submitAgentTarget() : await submitNewAgent();
if (submitted) {
onClose();
}
} catch (error) {
model.setSubmitError(toErrorMessage(error));
}
}, [canSubmit, model, onClose, state.targetKind, submitAgentTarget, submitNewAgent]);
const handleSubmitPress = useCallback(() => {
void handleSubmit();
}, [handleSubmit]);
const header = useMemo<SheetHeader>(() => {
if (mode !== "edit") {
return { title: "New schedule" };
}
return { title: schedule?.target.type === "agent" ? "Edit heartbeat" : "Edit schedule" };
}, [mode, schedule?.target.type]);
const footer = useMemo(
() => (
<View style={styles.footer}>
<Button
style={styles.footerButton}
variant="secondary"
onPress={onClose}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
style={styles.footerButton}
variant="default"
onPress={handleSubmitPress}
disabled={!canSubmit}
loading={isSubmitting}
testID="schedule-form-submit"
>
{mode === "edit" ? "Save changes" : "Create schedule"}
</Button>
</View>
),
[canSubmit, handleSubmitPress, isSubmitting, mode, onClose],
);
return (
<AdaptiveModalSheet
header={header}
visible={visible}
onClose={onClose}
onDismiss={onDismiss}
footer={footer}
testID="schedule-form-sheet"
>
<ScheduleFormFields
model={model}
state={state}
providerSnapshot={providerSnapshot}
agentTargetLabel={agentTargetLabel}
controlSize={controlSize}
cadenceError={cadenceError}
mutationServerId={mutationServerId}
/>
</AdaptiveModalSheet>
);
}
interface ScheduleFormFieldsProps {
model: ScheduleFormModel;
state: ScheduleFormState;
providerSnapshot: ReturnType<typeof useScheduleFormProviderSnapshot>;
agentTargetLabel: string | null;
controlSize: FieldControlSize;
cadenceError: string | null;
mutationServerId: string;
}
function ScheduleFormFields({
model,
state,
providerSnapshot,
agentTargetLabel,
controlSize,
cadenceError,
mutationServerId,
}: ScheduleFormFieldsProps): ReactElement {
if (state.targetKind === "agent") {
return (
<>
<ScheduleAgentTargetField label={agentTargetLabel} size={controlSize} />
<CadenceEditor
value={state.cadence}
onChange={model.setCadence}
error={cadenceError ?? undefined}
size={controlSize}
/>
{state.submitError ? <Text style={styles.submitError}>{state.submitError}</Text> : null}
</>
);
}
return (
<>
<Field label="Name">
<FormTextInput
size={controlSize}
testID="schedule-name-input"
accessibilityLabel="Schedule name"
initialValue={state.name}
value={state.name}
onChangeText={model.setName}
placeholder="Optional"
autoCapitalize="none"
autoCorrect={false}
/>
</Field>
<Field label="Prompt">
<FormTextInput
size={controlSize}
testID="schedule-prompt-input"
accessibilityLabel="Prompt"
initialValue={state.prompt}
value={state.prompt}
onChangeText={model.setPrompt}
placeholder="What should the agent do each run?"
style={styles.multilineInput}
multiline
numberOfLines={4}
textAlignVertical="top"
/>
</Field>
<ScheduleTargetFields
model={model}
state={state}
providerSnapshot={providerSnapshot}
agentTargetLabel={null}
controlSize={controlSize}
mutationServerId={mutationServerId}
/>
<CadenceEditor
value={state.cadence}
onChange={model.setCadence}
error={cadenceError ?? undefined}
size={controlSize}
/>
<Field label="Max runs">
<FormTextInput
size={controlSize}
testID="schedule-max-runs-input"
accessibilityLabel="Max runs"
initialValue={state.maxRuns}
value={state.maxRuns}
onChangeText={model.setMaxRuns}
placeholder="Unlimited"
keyboardType="number-pad"
/>
</Field>
{state.submitError ? <Text style={styles.submitError}>{state.submitError}</Text> : null}
</>
);
}
interface ScheduleTargetFieldsProps {
model: ScheduleFormModel;
state: ScheduleFormState;
providerSnapshot: ReturnType<typeof useScheduleFormProviderSnapshot>;
agentTargetLabel: string | null;
controlSize: FieldControlSize;
mutationServerId: string;
}
function ScheduleTargetFields({
model,
state,
providerSnapshot,
agentTargetLabel,
controlSize,
mutationServerId,
}: ScheduleTargetFieldsProps): ReactElement {
const hostOptions = useMemo<SelectFieldOption<string>[]>(
() =>
state.hosts.map((host) => ({
id: host.serverId,
value: host.serverId,
label: host.label,
testID: buildScheduleHostOptionTestId(host.serverId),
})),
[state.hosts],
);
const selectedHost = state.hosts.find((host) => host.serverId === state.selectedServerId) ?? null;
const selectedHostDisplay = useMemo<SelectFieldDisplay | null>(() => {
if (selectedHost) {
return { label: selectedHost.label };
}
if (state.selectedServerId) {
return { label: state.selectedServerId };
}
return null;
}, [selectedHost, state.selectedServerId]);
const projectOptions = state.projectOptions;
const modeOptions = useMemo<SelectFieldOption<string>[]>(
() =>
state.modeOptions.map((option) => ({
id: option.id,
value: option.id,
label: option.label,
})),
[state.modeOptions],
);
const thinkingOptions = useMemo<SelectFieldOption<string>[]>(
() =>
state.availableThinkingOptions.map((option) => ({
id: option.id,
value: option.id,
label: formatThinkingOptionLabel(option),
testID: buildThinkingOptionTestId(option.id),
})),
[state.availableThinkingOptions],
);
const handleSelectHost = useCallback(
(nextServerId: string) => {
model.setHost(nextServerId);
},
[model],
);
const handleSelectProject = useCallback(
(optionId: string, display: ScheduleFormDisplay) => {
model.setProject(optionId, display);
},
[model],
);
const handleSelectModel = useCallback(
(provider: AgentProvider, modelId: string) => {
model.setModel(provider, modelId);
},
[model],
);
const handleSelectMode = useCallback(
(modeId: string) => {
model.setSessionMode(modeId);
},
[model],
);
const handleSelectThinking = useCallback(
(thinkingOptionId: string) => {
model.setThinking(thinkingOptionId);
},
[model],
);
const handleModelOpen = useCallback(() => {
providerSnapshot.refetchIfStale(state.selectedProvider);
}, [providerSnapshot, state.selectedProvider]);
const handleRetryProvider = useCallback(
(provider: AgentProvider) => {
void providerSnapshot.refresh([provider]);
},
[providerSnapshot],
);
const renderHostOption = useCallback(
(input: SelectFieldRenderOptionInput<string>) => <HostOptionItem {...input} />,
[],
);
const renderProjectOption = useCallback(
(input: SelectFieldRenderOptionInput<string>) => <ProjectOptionItem {...input} />,
[],
);
const renderThinkingOption = useCallback(
(input: SelectFieldRenderOptionInput<string>) => <ThinkingOptionItem {...input} />,
[],
);
const modelTriggerLeading = useMemo(
() => <ProviderGlyph provider={state.selectedProvider} />,
[state.selectedProvider],
);
const renderModelTrigger = useCallback(
({
selectedModelLabel,
disabled,
isOpen,
hovered,
pressed,
}: {
selectedModelLabel: string;
onPress: () => void;
disabled: boolean;
isOpen: boolean;
hovered: boolean;
pressed: boolean;
}): ReactNode => {
const displayLabel = state.selectedModelDisplay?.label ?? selectedModelLabel;
return (
<SelectFieldTrigger
label={displayLabel}
isPlaceholder={!state.selectedModel}
placeholder={displayLabel}
leading={modelTriggerLeading}
disabled={disabled}
active={hovered || pressed || isOpen}
size={controlSize}
testID="schedule-model-trigger"
/>
);
},
[controlSize, modelTriggerLeading, state.selectedModel, state.selectedModelDisplay],
);
if (state.targetKind === "agent") {
return <ScheduleAgentTargetField label={agentTargetLabel} size={controlSize} />;
}
return (
<>
{state.mode === "edit" || state.hosts.length > 1 ? (
<SelectField
label="Host"
value={state.selectedServerId}
selectedDisplay={selectedHostDisplay}
options={hostOptions}
onChange={handleSelectHost}
placeholder="Select host"
emptyText="No hosts found"
disabled={state.mode === "edit"}
searchable={false}
title="Host"
size={controlSize}
triggerTestID="schedule-host-trigger"
renderOption={renderHostOption}
/>
) : null}
{state.disclosure.showProjectField ? (
<SelectField
label="Project"
value={state.selectedProjectOptionId || null}
selectedDisplay={state.projectDisplay}
options={projectOptions}
onChange={handleSelectProject}
placeholder="Select project"
emptyText="No projects found"
disabled={!state.selectedServerId}
hint={!state.selectedServerId ? "Choose a host first." : undefined}
searchable
searchPlaceholder="Search projects..."
title="Select project"
size={controlSize}
triggerTestID="schedule-project-trigger"
renderOption={renderProjectOption}
/>
) : null}
{state.disclosure.showModelField ? (
<Field label="Model">
<CombinedModelSelector
providers={state.modelSelectorProviders}
selectedProvider={state.selectedProvider ?? ""}
selectedModel={state.selectedModel}
onSelect={handleSelectModel}
isLoading={providerSnapshot.isLoading || providerSnapshot.isFetching}
renderTrigger={renderModelTrigger}
triggerFill
serverId={mutationServerId}
disabled={!state.selectedServerId}
onOpen={handleModelOpen}
onRetryProvider={handleRetryProvider}
isRetryingProvider={providerSnapshot.isRefreshing}
/>
</Field>
) : null}
{state.disclosure.showThinkingField ? (
<SelectField
label="Thinking"
value={state.selectedThinkingOptionId || null}
selectedDisplay={state.selectedThinkingDisplay}
options={thinkingOptions}
onChange={handleSelectThinking}
placeholder="Select thinking"
emptyText="No thinking options found"
searchable={thinkingOptions.length > 6}
title="Select thinking"
size={controlSize}
triggerTestID="schedule-thinking-trigger"
renderOption={renderThinkingOption}
/>
) : null}
{state.disclosure.showModeField ? (
<SelectField
label="Mode"
value={state.selectedMode || null}
selectedDisplay={state.selectedModeDisplay}
options={modeOptions}
onChange={handleSelectMode}
placeholder="Default mode"
emptyText="No modes found"
disabled={modeOptions.length === 0}
hint={modeOptions.length === 0 ? "No modes are available for this model." : undefined}
searchable={modeOptions.length > 6}
title="Select mode"
size={controlSize}
triggerTestID="schedule-mode-trigger"
/>
) : null}
{state.disclosure.showIsolationField ? (
<ScheduleIsolationField model={model} state={state} size={controlSize} />
) : null}
{state.disclosure.showArchiveOnFinishField ? (
<Field label="Archive on finish">
<Switch
value={state.archiveOnFinish}
onValueChange={model.setArchiveOnFinish}
accessibilityLabel="Archive on finish"
testID="schedule-archive-on-finish-switch"
/>
</Field>
) : null}
</>
);
}
function ScheduleIsolationField({
model,
state,
size,
}: {
model: ScheduleFormModel;
state: ScheduleFormState;
size: FieldControlSize;
}): ReactElement {
const options = useMemo<SelectFieldOption<"local" | "worktree">[]>(
() => [
{
id: "local",
value: "local",
label: "Local",
testID: "schedule-isolation-local",
},
{
id: "worktree",
value: "worktree",
label: "Worktree",
testID: "schedule-isolation-worktree",
},
],
[],
);
const selectedDisplay = useMemo<SelectFieldDisplay>(
() => ({ label: state.effectiveIsolation === "worktree" ? "Worktree" : "Local" }),
[state.effectiveIsolation],
);
const triggerLeading = useMemo(
() => (
<View style={styles.optionIconBox}>
{state.effectiveIsolation === "worktree" ? (
<GitBranch size={16} color={styles.providerIcon.color} />
) : (
<Folder size={16} color={styles.providerIcon.color} />
)}
</View>
),
[state.effectiveIsolation],
);
const handleSelectIsolation = useCallback(
(value: "local" | "worktree") => {
model.setIsolation(value);
},
[model],
);
const renderIsolationOption = useCallback(
(input: SelectFieldRenderOptionInput<"local" | "worktree">) => (
<IsolationOptionItem {...input} />
),
[],
);
return (
<SelectField
label="Isolation"
value={state.effectiveIsolation}
selectedDisplay={selectedDisplay}
options={options}
onChange={handleSelectIsolation}
placeholder="Select isolation"
emptyText="No isolation options found"
searchable={false}
title="Isolation"
size={size}
testID="schedule-isolation"
triggerTestID="schedule-isolation-trigger"
triggerLeading={triggerLeading}
renderOption={renderIsolationOption}
/>
);
}
function ScheduleAgentTargetField({
label,
size,
}: {
label: string | null;
size: FieldControlSize;
}): ReactElement {
const fieldStyle = useMemo(
() => [styles.readonlyField, size === "sm" ? styles.readonlyFieldSm : styles.readonlyFieldMd],
[size],
);
const textStyle = useMemo(
() => [styles.readonlyText, size === "sm" ? styles.readonlyTextSm : styles.readonlyTextMd],
[size],
);
return (
<Field label="Target">
<View style={fieldStyle} testID="schedule-agent-target">
<Text style={textStyle} numberOfLines={1}>
{label}
</Text>
</View>
</Field>
);
}
function IsolationOptionItem({
option,
selected,
active,
onPress,
}: SelectFieldRenderOptionInput<"local" | "worktree">): ReactElement {
const leadingSlot = useMemo(
() => (
<View style={styles.optionIconBox}>
{option.value === "worktree" ? (
<GitBranch size={16} color={styles.providerIcon.color} />
) : (
<Folder size={16} color={styles.providerIcon.color} />
)}
</View>
),
[option.value],
);
return (
<ComboboxItem
testID={option.testID}
label={option.label}
selected={selected}
active={active}
onPress={onPress}
leadingSlot={leadingSlot}
/>
);
}
function HostOptionItem({
option,
selected,
active,
onPress,
}: SelectFieldRenderOptionInput<string>): ReactElement {
const leadingSlot = useMemo(() => <HostStatusDotSlot serverId={option.value} />, [option.value]);
return (
<ComboboxItem
testID={option.testID}
label={option.label}
selected={selected}
active={active}
onPress={onPress}
leadingSlot={leadingSlot}
/>
);
}
function ProjectOptionItem({
option,
selected,
active,
onPress,
}: SelectFieldRenderOptionInput<string>): ReactElement {
const leadingSlot = useMemo(
() => (
<View style={styles.optionIconBox}>
<Folder size={16} color={styles.providerIcon.color} />
</View>
),
[],
);
return (
<ComboboxItem
testID={option.testID}
label={option.label}
selected={selected}
active={active}
onPress={onPress}
leadingSlot={leadingSlot}
/>
);
}
function ThinkingOptionItem({
option,
selected,
active,
onPress,
}: SelectFieldRenderOptionInput<string>): ReactElement {
const leadingSlot = useMemo(
() => (
<View style={styles.optionIconBox}>
<Brain size={16} color={styles.providerIcon.color} />
</View>
),
[],
);
return (
<ComboboxItem
testID={option.testID}
label={option.label}
selected={selected}
active={active}
onPress={onPress}
leadingSlot={leadingSlot}
/>
);
}
function ProviderGlyph({ provider }: { provider: string | null }): ReactElement | null {
if (!provider) {
return null;
}
const Icon = getProviderIcon(provider);
return <Icon size={16} color={styles.providerIcon.color} />;
}
const styles = StyleSheet.create((theme) => {
const geometry = createControlGeometry(theme);
return {
multilineInput: {
minHeight: 96,
},
readonlyField: {
flexDirection: "row",
alignItems: "center",
backgroundColor: theme.colors.surface2,
borderWidth: 1,
borderColor: theme.colors.border,
},
readonlyFieldSm: {
...geometry.formTextInputSm,
},
readonlyFieldMd: {
...geometry.formTextInputMd,
},
readonlyText: {
flex: 1,
minWidth: 0,
color: theme.colors.foreground,
},
readonlyTextSm: {
fontSize: theme.fontSize.sm,
},
readonlyTextMd: {
fontSize: theme.fontSize.base,
},
optionIconBox: {
width: 18,
height: 18,
alignItems: "center",
justifyContent: "center",
},
footer: {
flex: 1,
flexDirection: "row",
gap: theme.spacing[3],
},
footerButton: {
flex: 1,
},
submitError: {
color: theme.colors.palette.red[300],
fontSize: theme.fontSize.xs,
},
providerIcon: {
color: theme.colors.foregroundMuted,
},
};
});