Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 46 additions & 26 deletions apps/web/src/components/settings/ScheduledTasksSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
import { type ReactNode, useRef, useState } from "react";
import type {
EnvironmentId,
ModelSelection,
OrchestrationV2ThreadLaunchWorkspaceStrategy,
ProjectId,
ScheduledTask,
Expand All @@ -39,6 +38,7 @@ import {
ProviderInstanceId,
resolveEnvironmentMachineKind,
} from "@t3tools/contracts";
import { createModelSelection } from "@t3tools/shared/model";
import {
isAtomCommandInterrupted,
squashAtomCommandFailure,
Expand Down Expand Up @@ -90,6 +90,7 @@ 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 { readPromptUltrathink, TraitsPicker } from "../chat/TraitsPicker";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import {
Expand Down Expand Up @@ -140,10 +141,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: "",
Expand Down Expand Up @@ -272,15 +272,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") {
Expand Down Expand Up @@ -1150,12 +1141,15 @@ function ScheduledTaskEditorForm({
);

const firstInstance = instanceEntries[0];
const draftSelection = draft.modelKey ? splitModelKey(draft.modelKey) : null;
const draftSelection = draft.modelSelection;
const activeSelection =
draftSelection &&
instanceEntries.some((entry) => entry.instanceId === draftSelection.instanceId && entry.enabled)
? draftSelection
: scheduledTaskDefaultModel(settings, selectedProject ?? null, instanceEntries);
const activeEntry = instanceEntries.find(
(entry) => entry.instanceId === activeSelection?.instanceId,
);
const activeInstanceId =
activeSelection?.instanceId ?? firstInstance?.instanceId ?? ("" as ProviderInstanceId);
const activeModel = activeSelection?.model ?? "";
Expand Down Expand Up @@ -1227,12 +1221,6 @@ function ScheduledTaskEditorForm({
reportFailure("Checkout path is required", "Enter the path of the checkout to run in.");
return;
}
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" }
Expand All @@ -1252,7 +1240,7 @@ function ScheduledTaskEditorForm({
projectId: selectedProjectId as ProjectId,
threadId: draft.threadId ? (draft.threadId as ThreadId) : null,
workspaceStrategy,
modelSelection,
modelSelection: selection,
runtimeMode: draft.runtimeMode,
interactionMode: draft.interactionMode,
creationSource: "web",
Expand Down Expand Up @@ -1298,8 +1286,7 @@ function ScheduledTaskEditorForm({
setDraft((current) => ({
...current,
projectId: "",
modelKey: "",
baseModelSelection: null,
modelSelection: null,
baseRef: "main",
startFromOrigin: true,
existingWorktreePath: "",
Expand Down Expand Up @@ -1466,7 +1453,7 @@ function ScheduledTaskEditorForm({
</Field>

<Field label="Model">
<div>
<div className="flex flex-wrap items-center gap-1.5">
<ProviderModelPicker
environmentId={environmentId}
disabled={saving || !connected}
Expand All @@ -1477,10 +1464,43 @@ function ScheduledTaskEditorForm({
modelOptionsByInstance={modelOptionsByInstance}
isComposerOwned={false}
triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
onInstanceModelChange={(instanceId, model) =>
setDraft((current) => ({ ...current, modelKey: `${instanceId}:${model}` }))
}
onInstanceModelChange={(instanceId, model) => {
// Options belong to one model, so a new model starts from its own defaults.
if (activeSelection?.instanceId === instanceId && activeSelection.model === model)
return;
setDraft((current) => ({
...current,
modelSelection: createModelSelection(instanceId, model),
}));
}}
/>
{activeSelection && activeEntry ? (
<TraitsPicker
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
provider={activeEntry.driverKind}
instanceId={activeSelection.instanceId}
models={activeEntry.models}
model={activeSelection.model}
modelOptions={activeSelection.options ?? []}
// Ultrathink is a prompt prefix, not a stored option, so it edits the task prompt.
promptUltrathink={readPromptUltrathink(draft.prompt)}
onPromptEdit={(edit) =>
setDraft((current) => ({ ...current, prompt: edit(current.prompt) }))
Comment thread
kacigaya marked this conversation as resolved.
Resolved
}
planModeEnabled={settings.planModeEnabled}
disabled={saving || !connected}
triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
onModelOptionsChange={(options) =>
setDraft((current) => ({
...current,
modelSelection: createModelSelection(
activeSelection.instanceId,
activeSelection.model,
options,
),
}))
}
/>
) : null}
</div>
</Field>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ describe("editing scheduled task branch settings", () => {
expect(draft.baseRef).toBe("release");
expect(draft.startFromOrigin).toBe(false);
});

it("keeps the saved reasoning level", () => {
const modelSelection = {
...legacyTask.modelSelection,
options: [{ id: "reasoningEffort", value: "high" }],
};
expect(taskToDraft({ ...legacyTask, modelSelection }).modelSelection).toEqual(modelSelection);
});
});

describe("webhook scheduled tasks", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,10 @@ export interface DraftState {
readonly baseRef: string;
readonly startFromOrigin: boolean;
readonly existingWorktreePath: string;
readonly modelKey: string;
/** Null follows the project's default model. Provider options carry the reasoning level. */
readonly modelSelection: ModelSelection | null;
readonly runtimeMode: RuntimeMode;
readonly interactionMode: ProviderInteractionMode;
/** Retain the source selection's provider options when the picker leaves instanceId:model unchanged. */
readonly baseModelSelection: ModelSelection | null;
readonly signatureEnabled: boolean;
readonly signatureHeader: string;
readonly signatureEncoding: "hex" | "base64";
Expand Down Expand Up @@ -224,10 +223,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,
Expand Down
Loading