diff --git a/apps/web/src/components/settings/ScheduledTasksSettings.tsx b/apps/web/src/components/settings/ScheduledTasksSettings.tsx index 7b941f97a79e..cfb08f408beb 100644 --- a/apps/web/src/components/settings/ScheduledTasksSettings.tsx +++ b/apps/web/src/components/settings/ScheduledTasksSettings.tsx @@ -12,7 +12,6 @@ import { import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { EnvironmentId, - ModelSelection, OrchestrationV2ThreadLaunchWorkspaceStrategy, ProjectId, ScheduledTask, @@ -24,6 +23,7 @@ import type { ThreadId, } from "@t3tools/contracts"; import { DEFAULT_WEBHOOK_PROMPT } from "@t3tools/client-runtime/scheduled-task-webhook"; +import { createModelSelection } from "@t3tools/shared/model"; import { MAX_WEBHOOK_DELIVERY_AGE_MINUTES, MIN_SCHEDULED_TASK_INTERVAL_MS, @@ -77,6 +77,8 @@ import { Menu, MenuTrigger, MenuPopup, MenuItem, MenuSeparator } from "../ui/men import { ToggleGroup, Toggle } from "../ui/toggle-group"; import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyMedia } from "../ui/empty"; import { ProviderModelPicker } from "../chat/ProviderModelPicker"; +import { runtimeModeConfig, runtimeModeOptions } from "../chat/runtimeModeConfig"; +import { TraitsPicker } from "../chat/TraitsPicker"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { @@ -128,10 +130,9 @@ const EMPTY_DRAFT: DraftState = { baseRef: "main", startFromOrigin: true, existingWorktreePath: "", - modelKey: "", + modelSelection: null, runtimeMode: "full-access", interactionMode: "default", - baseModelSelection: null, signatureEnabled: false, ...WEBHOOK_SIGNATURE_DEFAULTS, signatureSecret: "", @@ -163,15 +164,6 @@ function Field({ ); } -function splitModelKey(value: string): ModelSelection | null { - const index = value.indexOf(":"); - if (index <= 0 || index === value.length - 1) return null; - return { - instanceId: ProviderInstanceId.make(value.slice(0, index)), - model: value.slice(index + 1), - }; -} - export function scheduleLabel(schedule: ScheduledTaskSchedule): string { if (schedule.type === "webhook") return "On webhook"; if (schedule.type === "interval") { @@ -828,15 +820,25 @@ function ScheduledTaskEditorDialog({ const selectedProjectId = draft.projectId || projects[0]?.id || ""; const selectedProject = projects.find((project) => project.id === selectedProjectId); - // The real model picker is keyed by a `${instanceId}:${model}` string, which - // is exactly how the draft stores its selection. const firstInstance = instanceEntries[0]; - const activeSelection = draft.modelKey - ? splitModelKey(draft.modelKey) - : scheduledTaskDefaultModel(settings, selectedProject ?? null, instanceEntries); + const activeSelection = + draft.modelSelection ?? + scheduledTaskDefaultModel(settings, selectedProject ?? null, instanceEntries); const activeInstanceId = activeSelection?.instanceId ?? firstInstance?.instanceId ?? ("" as ProviderInstanceId); const activeModel = activeSelection?.model ?? ""; + const activeEntry = instanceEntries.find((entry) => entry.instanceId === activeInstanceId); + const supportedRuntimeModes = activeEntry?.snapshot.supportedRuntimeModes; + const compatibleRuntimeModes = + supportedRuntimeModes && supportedRuntimeModes.length > 0 + ? runtimeModeOptions.filter((mode) => supportedRuntimeModes.includes(mode)) + : runtimeModeOptions; + // A mode the selected provider does not offer would run as Supervised, so the + // dialog shows and saves the provider's first supported mode instead. + const runtimeMode = compatibleRuntimeModes.includes(draft.runtimeMode) + ? draft.runtimeMode + : (compatibleRuntimeModes[0] ?? draft.runtimeMode); + const PermissionIcon = runtimeModeConfig[runtimeMode].icon; const modelOptionsByInstance = useMemo( () => getCustomModelOptionsByInstance(settings, providers, activeInstanceId, activeModel), [settings, providers, activeInstanceId, activeModel], @@ -901,14 +903,6 @@ function ScheduledTaskEditorDialog({ reportFailure("Checkout path is required", "Enter the path of the checkout to run in."); return; } - // Keep the original selection object (with provider options) when the - // picker still points at the same instance+model. - const modelSelection = - draft.baseModelSelection !== null && - draft.baseModelSelection.instanceId === selection.instanceId && - draft.baseModelSelection.model === selection.model - ? draft.baseModelSelection - : selection; const workspaceStrategy: OrchestrationV2ThreadLaunchWorkspaceStrategy = draft.workspaceMode === "root" ? { type: "root" } @@ -928,8 +922,8 @@ function ScheduledTaskEditorDialog({ projectId: selectedProjectId as ProjectId, threadId: draft.threadId ? (draft.threadId as ThreadId) : null, workspaceStrategy, - modelSelection, - runtimeMode: draft.runtimeMode, + modelSelection: selection, + runtimeMode, interactionMode: draft.interactionMode, creationSource: "web", }; @@ -980,8 +974,7 @@ function ScheduledTaskEditorDialog({ setDraft((current) => ({ ...current, projectId: "", - modelKey: "", - baseModelSelection: null, + modelSelection: null, baseRef: "main", startFromOrigin: true, existingWorktreePath: "", @@ -1121,22 +1114,93 @@ function ScheduledTaskEditorDialog({ - - setDraft((current) => ({ ...current, modelKey: `${instanceId}:${model}` })) - } - /> +
+ { + // Re-picking the current model keeps its options. + if ( + activeSelection?.instanceId === instanceId && + activeSelection.model === model + ) + return; + setDraft((current) => ({ + ...current, + modelSelection: createModelSelection(instanceId, model), + })); + }} + /> + {activeSelection && activeEntry ? ( + setDraft((current) => ({ ...current, prompt }))} + modelOptions={activeSelection.options ?? []} + planModeEnabled={settings.planModeEnabled} + triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME} + onModelOptionsChange={(options) => + setDraft((current) => ({ + ...current, + modelSelection: createModelSelection( + activeSelection.instanceId, + activeSelection.model, + options, + ), + })) + } + /> + ) : null} +
+ {/* A task bound to a thread runs with that thread's permissions. */} + {draft.threadId ? null : ( + + + + )} +
{task?.schedule.type === "interval" && task.schedule.everyMs < MIN_SCHEDULED_TASK_INTERVAL_MS ? ( diff --git a/apps/web/src/components/settings/scheduledTasksSettings.logic.ts b/apps/web/src/components/settings/scheduledTasksSettings.logic.ts index 6c738ce0eff0..3e2986bc39de 100644 --- a/apps/web/src/components/settings/scheduledTasksSettings.logic.ts +++ b/apps/web/src/components/settings/scheduledTasksSettings.logic.ts @@ -63,16 +63,11 @@ export interface DraftState { readonly baseRef: string; readonly startFromOrigin: boolean; readonly existingWorktreePath: string; - readonly modelKey: string; - /** Not editable in the dialog, but preserved so editing an agent-created task keeps its modes. */ + /** Null follows the project's default model, including its provider options. */ + readonly modelSelection: ModelSelection | null; readonly runtimeMode: RuntimeMode; + /** Not editable in the dialog, but preserved so editing an agent-created task keeps its mode. */ readonly interactionMode: ProviderInteractionMode; - /** - * The task's original model selection. The picker only edits - * `instanceId:model`; keeping the source object preserves provider options - * (reasoning, temperature, …) when the model itself is left unchanged. - */ - readonly baseModelSelection: ModelSelection | null; readonly signatureEnabled: boolean; readonly signatureHeader: string; readonly signatureEncoding: "hex" | "base64"; @@ -150,10 +145,9 @@ export function taskToDraft(task: ScheduledTask): DraftState { task.workspaceStrategy.type === "existing_worktree" ? task.workspaceStrategy.worktreePath : "", - modelKey: `${task.modelSelection.instanceId}:${task.modelSelection.model}`, + modelSelection: task.modelSelection, runtimeMode: task.runtimeMode, interactionMode: task.interactionMode, - baseModelSelection: task.modelSelection, ...(schedule.type === "webhook" && schedule.signature !== null ? { signatureEnabled: true,