diff --git a/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx b/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx index 124a27c7b11f4..7e42bba4bc6c8 100644 --- a/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx +++ b/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx @@ -6,6 +6,7 @@ import { ProjectId, ProviderInstanceId, ScheduledTaskId, + ThreadId, type ScheduledTask, } from "@t3tools/contracts"; import { describe, expect, it, vi } from "vite-plus/test"; @@ -24,7 +25,7 @@ const task: ScheduledTask = { enabled: true, schedule: { type: "interval", everyMs: 60_000 }, projectId, - threadId: null, + threadId: ThreadId.make("older-thread"), workspaceStrategy: { type: "root" }, modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", @@ -40,6 +41,12 @@ const task: ScheduledTask = { runCount: 0, }; +// Stands in for the thread family read the editor makes when the shell lacks the bound thread. +const threadFamilyRead = { + snapshotSequence: 1, + threads: [{ id: task.threadId, title: "Fix login bug" }], +}; + const state = vi.hoisted(() => ({ connected: true, tasksLoaded: true, secondary: false })); const secondaryId = EnvironmentId.make("secondary"); const permissions = new Map>(); @@ -102,11 +109,21 @@ vi.mock(import("../../state/server"), async (original) => { }, }; }); +vi.mock(import("../../state/threads"), async (original) => { + const actual = await original(); + return { + ...actual, + threadFamilies: { ...actual.threadFamilies, familyAtom: (() => threadFamilyRead) as never }, + }; +}); // Mirrors the real query: a null request (disconnected environment) has no data. vi.mock(import("../../state/query"), async (original) => ({ ...(await original()), useEnvironmentQuery: ((request: unknown) => ({ - data: request === null ? null : { tasks: state.tasksLoaded ? [task] : [] }, + data: + request === null || request === threadFamilyRead + ? request + : { tasks: state.tasksLoaded ? [task] : [] }, error: null, isPending: false, refresh: vi.fn(), @@ -166,6 +183,7 @@ describe("ScheduledTasksSettings", () => { it("keeps a linked task's unsaved edits across a disconnect and reconnect", async () => { setup(); await using screen = await render(); + await expect.element(page.getByRole("dialog").getByText("Fix login bug")).toBeVisible(); const name = page.getByRole("dialog").getByRole("textbox", { name: "Name" }); await expect.element(name).toHaveValue("Review issues"); await userEvent.fill(name, "Review stale issues"); diff --git a/apps/web/src/components/settings/ScheduledTasksSettings.tsx b/apps/web/src/components/settings/ScheduledTasksSettings.tsx index e027438a6abdb..0516a52fe3b8d 100644 --- a/apps/web/src/components/settings/ScheduledTasksSettings.tsx +++ b/apps/web/src/components/settings/ScheduledTasksSettings.tsx @@ -59,10 +59,12 @@ import { useEnvironmentHttpBaseUrl, type EnvironmentPresentation, } from "../../state/environments"; -import { useProjects } from "../../state/entities"; +import { useProjects, useThreadShell } from "../../state/entities"; +import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { environmentAtomList, environmentIdsKey } from "../../state/environmentAtomList"; import { formatEnvironmentQueryError, useEnvironmentQuery } from "../../state/query"; import { EMPTY_SERVER_PROVIDERS, serverEnvironment } from "../../state/server"; +import { threadFamilies } from "../../state/threads"; import { useAtomCommand } from "../../state/use-atom-command"; import { WorktreeBaseBranchPicker } from "../WorktreeBaseBranchPicker"; import { EnvironmentMachineIcon } from "../EnvironmentMachineIcon"; @@ -1108,6 +1110,38 @@ function ScheduledTaskEditorDialog({ ); } +function ScheduledTaskDestination({ + environmentId, + threadId, +}: { + readonly environmentId: EnvironmentId; + readonly threadId: ThreadId | null; +}) { + const ref = threadId === null ? null : scopeThreadRef(environmentId, threadId); + const shell = useThreadShell(ref); + // The shell can omit older settled threads; their family read still carries the title. + const family = useEnvironmentQuery( + ref !== null && shell === null ? threadFamilies.familyAtom(environmentId, ref.threadId) : null, + ); + const thread = shell ?? family.data?.threads.find((row) => row.id === threadId); + return ( + + {ref === null ? ( +

+ Each run starts a new thread with the workspace and model below. +

+ ) : ( +
+

{thread?.title.trim() || `Thread ${ref.threadId}`}

+

+ Each run continues this conversation, in its workspace and with its current model. +

+
+ )} +
+ ); +} + function ScheduledTaskEditorForm({ initialEnvironmentId, task, @@ -1384,6 +1418,11 @@ function ScheduledTaskEditorForm({ /> + +