import { ActiveNewTaskContext } from "../../state/active-new-task"; import * as Dialog from "../../components/ui/Dialog"; import React, { useEffect, useRef, useState } from "react"; import type { EnvironmentId, ModelSelection, ProviderInteractionMode, ProviderOptionSelection, RuntimeMode, ServerProvider, } from "@t3tools/contracts"; import { CommandId, DEFAULT_PROVIDER_INTERACTION_MODE, MessageId, repositoryGroupingKeyOf, ThreadId, } from "@t3tools/contracts"; import { threadContextsOutsideEnvironment } from "@t3tools/shared/composerContextReferences"; import * as Arr from "effect/Array"; import { pipe } from "effect/Function"; import { useProjects, useServerConfigs, useThreadShells } from "../../state/entities"; import type { TurnCommandMetadata } from "../../lib/commandMetadata"; import type { DraftComposerAttachment } from "../../lib/composerImages"; import type { ModelOption, ProviderGroup } from "../../lib/modelOptions"; import { buildModelOptions, groupByProvider, resolveDefaultableModelSelection, resolveNewTaskModelSelection, resolveSelectableModelSelection, } from "../../lib/modelOptions"; import { scopedProjectKey } from "../../lib/scopedEntities"; import { useAtomValue } from "@effect/atom-react"; import { appAtomRegistry } from "../../state/atom-registry"; import { projectEnvironment } from "../../state/projects"; import { useEnvironmentQuery } from "../../state/query"; import { useAtomCommand } from "../../state/use-atom-command"; import { appendComposerDraftAttachments, type ComposerDraftInsertion, type ComposerDraftWorkspaceSelection, composerDraftsLoadedAtom, createNewTaskDraft, getComposerDraftSnapshot, isNewTaskDraftKey, removeComposerDraftAttachment, replaceComposerDraftAttachments, retargetNewTaskDraft, setComposerDraftText, setStickyComposerModelSelection, updateComposerDraftSettings, useComposerDraft, useStickyComposerModelSelection, } from "../../state/use-composer-drafts"; import { rememberModelOptions, withRememberedModelOptions, } from "../../state/use-model-option-memory"; import { useBranches, usePaginatedBranches } from "../../state/queries"; import { vcsEnvironment } from "../../state/vcs"; import { type QueuedThreadMessage } from "../../state/thread-outbox"; import { setPendingConnectionError, useRemoteConnectionStatus, useSavedRemoteConnections, } from "../../state/use-remote-environment-registry"; import { availableScratchWorkspaceRoot } from "@t3tools/client-runtime/operations/projects"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; import { type VcsRef } from "@t3tools/client-runtime/state/vcs"; import { buildHomeProjectScopes, sortHomeProjectScopes, type HomeProjectScope, } from "../home/homeThreadList"; import { useMobileProjectGroupingSettings } from "../../state/project-grouping"; import { resolvePendingTaskInteractionMode, resolveProviderInteractionMode, } from "../../state/legacy-plan-mode"; import { useLegacyPlanModeState } from "./use-legacy-plan-mode-enabled"; import { resolveNewTaskDraftWorkspace, resolveNewTaskBranchWorktreePath, resolveNewTaskLocalWorkspaceSelection, } from "./new-task-context-presentation"; import { resolveEnvironmentProjectMatch } from "./new-task-project-selection"; import { useNewTaskProjectDefaults } from "./use-new-task-project-defaults"; import { resolveProjectThreadCreationBranch } from "./projectThreadCreationValidation"; import { INACTIVE_FLOW_KEY, type NewTaskFlowState, newTaskFlowStateAtom, readNewTaskFlowState, resolveRouteDraft, type RouteDraftResolution, stampedEnvironmentProjectsLoading, updateNewTaskFlowState, } from "./new-task-flow-state"; import { useEnvironmentShellReadiness } from "../../state/shell"; import { useConnectionsReady, useWorkspaceEnvironments } from "../../state/workspace"; type WorkspaceMode = "local" | "worktree"; export function branchBadgeLabel(input: { readonly branch: VcsRef; readonly project: EnvironmentProject | null; }): string | null { if (input.branch.current) { return "current"; } if (input.branch.worktreePath && input.branch.worktreePath !== input.project?.workspaceRoot) { return "worktree"; } if (input.branch.isRemote) { return "remote"; } if (input.branch.isDefault) { return "default"; } return null; } type NewTaskFlowContextValue = { readonly activeNewTask: { readonly pendingKey: string | null } | null; readonly projectScopes: ReadonlyArray; readonly selectedEnvironmentId: EnvironmentId | null; readonly selectedProjectKey: string | null; readonly selectedModelKey: string | null; readonly workspaceMode: WorkspaceMode; readonly isGitRepo: boolean; readonly isGitRepoKnown: boolean; readonly canChooseWorkspace: boolean; readonly selectedBranchName: string | null; readonly selectedWorktreePath: string | null; readonly resolveWorkspaceSelection: ( selection: ComposerDraftWorkspaceSelection | undefined, ) => ComposerDraftWorkspaceSelection; readonly startFromOrigin: boolean; readonly pullRequestReference: string | undefined; readonly draftKey: string | null; readonly prompt: string; readonly attachments: ReadonlyArray; readonly submitting: boolean; readonly branchesLoading: boolean; readonly availableBranches: ReadonlyArray; readonly currentCheckoutBranchName: string | null; readonly runtimeMode: RuntimeMode; readonly interactionMode: ProviderInteractionMode; readonly planModeEnabled: boolean; readonly expandedProvider: string | null; readonly environments: ReadonlyArray<{ readonly environmentId: EnvironmentId; readonly environmentLabel: string; }>; readonly selectedProject: EnvironmentProject | null; readonly isScratchDraft: boolean; readonly modelOptions: ReadonlyArray; readonly selectedModel: ModelSelection | null; readonly selectedModelOption: ModelOption | null; readonly selectedProviderStatus: ServerProvider | null; readonly providerGroups: ReadonlyArray; readonly setProject: (project: EnvironmentProject) => void; /** Whether the selected machine can move this draft to its Scratch project. */ readonly canStartWithoutProject: boolean; readonly startWithoutProject: () => Promise; /** The flow's project, read when a continuation resumes after an await. */ readonly getSelectedProjectKey: () => string | null; /** * The draft id of the route this flow serves when that draft, or its * project, is gone. Only the topmost draft route is served, so a screen * compares this with its own draft id. */ readonly missingRouteDraftId: string | null; readonly selectEnvironment: (environmentId: EnvironmentId) => void; readonly switchEnvironment: (environmentId: EnvironmentId) => Promise; readonly switchingToEnvironmentId: EnvironmentId | null; readonly setSelectedModelKey: ( key: string | null, options?: ReadonlyArray, ) => void; readonly setWorkspaceMode: (mode: WorkspaceMode) => void; readonly selectBranch: (branch: VcsRef) => void; readonly setStartFromOrigin: (value: boolean) => void; readonly buildPendingTaskMessage: ( metadata: TurnCommandMetadata, options?: { readonly currentCheckoutBranch?: string | null; }, ) => QueuedThreadMessage | null; readonly setPrompt: (value: string) => void; readonly replaceAttachments: (attachments: ReadonlyArray) => void; /** Appends draft attachments; returns how many the live cap rejected. */ readonly appendAttachments: ( attachments: ReadonlyArray, insertion?: ComposerDraftInsertion, ) => number; readonly removeAttachment: (imageId: string) => void; readonly clearAttachments: () => void; readonly setSubmitting: (value: boolean) => void; readonly loadBranches: () => void; readonly setRuntimeMode: (value: RuntimeMode) => void; readonly setInteractionMode: (value: ProviderInteractionMode) => void; readonly setSelectedModelOptions: ( value: ReadonlyArray | undefined, ) => void; readonly setExpandedProvider: (value: string | null) => void; }; const NewTaskFlowContext = React.createContext(null); export function NewTaskFlowProvider( props: React.PropsWithChildren<{ active?: boolean; /** Identifies the draft route this flow serves; a new key starts a fresh flow. */ flowKey?: string; routeDraftId?: string; }>, ) { const projects = useProjects(); const threads = useThreadShells(); const { savedConnectionsById } = useSavedRemoteConnections(); const groupingSettings = useMobileProjectGroupingSettings(); const { enabled: legacyPlanModeEnabled } = useLegacyPlanModeState(); const projectScopes = sortHomeProjectScopes({ scopes: buildHomeProjectScopes({ projects, environmentId: null, projectGroupingMode: groupingSettings.sidebarProjectGroupingMode, }), threads, pendingTasks: [], projectSortOrder: "updated_at", }); const flowKey = props.flowKey ?? INACTIVE_FLOW_KEY; const storedFlowState = useAtomValue(newTaskFlowStateAtom(flowKey)); const draftsLoaded = useAtomValue(composerDraftsLoadedAtom); // A draft route opened from a list row, a deep link, or restored state binds // its draft here until the flow's first write stores the binding. const routeDraft = useComposerDraft( storedFlowState.draftKey === null ? (props.routeDraftId ?? null) : null, ); // Readiness of the environment the route's draft is stamped to; another // environment's projects loading first says nothing about it. const stampedEnvironmentId = routeDraft.project?.environmentId ?? null; const connectionsReady = useConnectionsReady(); const stampedEnvironmentConnection = useWorkspaceEnvironments().find( (environment) => environment.environmentId === stampedEnvironmentId, )?.connectionState ?? null; const stampedEnvironmentShell = useEnvironmentShellReadiness(stampedEnvironmentId); const stampedEnvironmentLoading = stampedEnvironmentProjectsLoading({ connectionsReady, connectionState: stampedEnvironmentConnection, shellCacheRead: stampedEnvironmentShell.cacheRead, shellStatus: stampedEnvironmentShell.status, shellHasError: stampedEnvironmentShell.hasError, }); const resolveRoute = ( flow: NewTaskFlowState, loaded: boolean, stamp: Parameters[0]["stamp"], ): RouteDraftResolution => resolveRouteDraft({ flow, routeDraftId: props.routeDraftId, draftsLoaded: loaded, stamp, projects, stampedEnvironmentLoading, }); const routeDraftResolution = resolveRoute(storedFlowState, draftsLoaded, routeDraft.project); const flowState = routeDraftResolution.kind === "open" ? routeDraftResolution.flow : storedFlowState; const { environmentId: selectedEnvironmentIdOverride, projectKey: selectedProjectKey, draftKey: activeDraftKey, submitting, expandedProvider, } = flowState; // Continuations read the flow at use time, with the route's draft applied. const readFlow = (): NewTaskFlowState => { const stored = readNewTaskFlowState(flowKey); const resolution = resolveRoute( stored, appAtomRegistry.get(composerDraftsLoadedAtom), props.routeDraftId === undefined ? undefined : getComposerDraftSnapshot(props.routeDraftId).project, ); return resolution.kind === "open" ? resolution.flow : stored; }; // Every flow write stores the route's draft binding along with it, so the // binding outlives the draft's project stamp (submit clears it). const updateFlow = (patch: Partial) => updateNewTaskFlowState(flowKey, { ...readFlow(), ...patch }); const selectedEnvironmentId = selectedEnvironmentIdOverride !== null && projects.some((project) => project.environmentId === selectedEnvironmentIdOverride) ? selectedEnvironmentIdOverride : (projects[0]?.environmentId ?? null); const projectsForEnvironment = pipe( projects, Arr.filter((project) => project.environmentId === selectedEnvironmentId), ); // A route draft still waiting for its environment has no project yet: the // fallback would show, and could send to, another environment's project. const selectedProject = routeDraftResolution.kind === "pending" ? null : (projectsForEnvironment.find( (project) => scopedProjectKey(project.environmentId, project.id) === selectedProjectKey, ) ?? projectsForEnvironment[0] ?? null); const projectDefaults = useNewTaskProjectDefaults(selectedProject); const selectedEnvironmentServerConfig = projectDefaults.serverConfig; const isScratchDraft = projectDefaults.isScratch; const serverConfigs = useServerConfigs(); const { connectedEnvironments } = useRemoteConnectionStatus(); const scratchEnvironments = connectedEnvironments.filter( (environment) => availableScratchWorkspaceRoot( environment.connectionState, serverConfigs.get(environment.environmentId), ) !== null, ); const selectedRepositoryKey = selectedProject?.repositoryIdentity ? repositoryGroupingKeyOf(selectedProject.repositoryIdentity) : null; // Use || null: placeholder workspaceRoot can be empty, and an empty basename rejects real hosts. const selectedWorkspaceBasename = selectedProject?.workspaceRoot.split("/").at(-1) || null; const selectedProjectTitle = selectedProject?.title ?? null; const environments = (() => { if (isScratchDraft) { return scratchEnvironments.map((environment) => ({ environmentId: environment.environmentId, environmentLabel: environment.environmentLabel, })); } const seen = new Set(); const result: Array<{ readonly environmentId: EnvironmentId; readonly environmentLabel: string; }> = []; const hostsSelectedRepository = (project: EnvironmentProject) => { if (selectedRepositoryKey === null && selectedWorkspaceBasename === null) { return true; } const projectKey = project.repositoryIdentity ? repositoryGroupingKeyOf(project.repositoryIdentity) : null; if (selectedRepositoryKey !== null && projectKey !== null) { return projectKey === selectedRepositoryKey; } return ( project.workspaceRoot.split("/").at(-1) === selectedWorkspaceBasename || (selectedProjectTitle !== null && project.title === selectedProjectTitle) ); }; for (const project of projects) { if (!hostsSelectedRepository(project)) { continue; } if (seen.has(project.environmentId)) { continue; } const environment = savedConnectionsById[project.environmentId]; if (!environment) { continue; } seen.add(project.environmentId); result.push({ environmentId: project.environmentId, environmentLabel: environment.environmentLabel, }); } return result; })(); const selectedProjectDraftKey = selectedProject ? activeDraftKey : null; // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- creates the persistent draft in the new-task flow store once a project is selected, before the composer mounts useEffect(() => { // A route that names a draft binds that draft, or returns to the picker. if ( props.active === false || props.routeDraftId !== undefined || activeDraftKey !== null || selectedProject === null ) { return; } // Creates the persistent draft once the project selection is ready; the key must exist before the composer mounts. updateNewTaskFlowState(flowKey, { draftKey: createNewTaskDraft({ environmentId: selectedProject.environmentId, projectId: selectedProject.id, }), }); }, [activeDraftKey, flowKey, selectedProject, props.active, props.routeDraftId]); const activePendingKey = props.active === false ? null : props.routeDraftId ? `draft-task:${props.routeDraftId}` : selectedProjectDraftKey ? `draft-task:${selectedProjectDraftKey}` : null; const activeNewTask = React.useMemo( () => (props.active === false ? null : { pendingKey: activePendingKey }), [props.active, activePendingKey], ); const selectedProjectDraft = useComposerDraft(selectedProjectDraftKey); const prompt = selectedProjectDraft.text; const attachments = selectedProjectDraft.attachments; const projectGitStatus = useEnvironmentQuery( selectedProject !== null && selectedProject.workspaceRoot !== "" ? vcsEnvironment.status({ environmentId: selectedProject.environmentId, input: { cwd: selectedProject.workspaceRoot }, }) : null, ); const currentCheckoutBranchName = projectGitStatus.data?.refName ?? null; // The unfiltered ref list also provides repository capability while offline. const projectBranches = useBranches({ environmentId: selectedProject?.environmentId ?? null, cwd: selectedProject?.workspaceRoot || null, }); const knownIsGitRepo = projectGitStatus.data?.isRepo ?? projectBranches.data?.isRepo ?? null; const isGitRepo = knownIsGitRepo ?? true; const projectSettings = projectDefaults.projectSettings; const canChooseWorkspace = !isScratchDraft; const defaultWorkspaceMode: WorkspaceMode = projectDefaults.defaultWorkspaceMode; const branchState = usePaginatedBranches({ environmentId: selectedProject?.environmentId ?? null, cwd: selectedProject?.workspaceRoot || null, }); const branchesLoading = branchState.isPending && branchState.data === null; const allBranchRefs = branchState.refs; const availableBranches = pipe( allBranchRefs, Arr.filter((branch) => !branch.isRemote), ); const resolveWorkspaceSelection = (selection: ComposerDraftWorkspaceSelection | undefined) => resolveNewTaskDraftWorkspace({ selection, canChooseWorkspace, defaultMode: defaultWorkspaceMode, defaultModeSettled: projectDefaults.defaultWorkspaceModeSettled, isGitRepo, branches: allBranchRefs, projectCwd: selectedProject?.workspaceRoot ?? null, }); const workspaceSelection = resolveWorkspaceSelection(selectedProjectDraft.workspaceSelection); const workspaceMode = workspaceSelection.mode; const selectedBranchName = workspaceSelection.branch; const selectedWorktreePath = workspaceSelection.worktreePath; // Keep the user's explicit choice separate from the resolved display value: // only the explicit flag is ever written back to the draft, so the resolved // value keeps tracking the server setting when the config loads late. const draftStartFromOrigin = selectedProjectDraft.workspaceSelection?.startFromOrigin; const startFromOrigin = isGitRepo && (draftStartFromOrigin ?? projectSettings.settings.newWorktreesStartFromOrigin); const defaultRuntimeMode = projectSettings.settings.defaultRuntimeMode; const runtimeMode = selectedProjectDraft.runtimeMode ?? defaultRuntimeMode; // Antigravity keeps unavailable selections so sign-out or a catalog change // cannot switch the user's model. const draftModelSelection = resolveSelectableModelSelection( selectedEnvironmentServerConfig, selectedProjectDraft.modelSelection ?? null, selectedProject?.id ?? null, ); const projectDefaultModelSelection = resolveDefaultableModelSelection( selectedEnvironmentServerConfig, projectSettings.settings.defaultModelSelection, selectedProject?.id ?? null, ); const storedStickyModelSelection = useStickyComposerModelSelection(); const stickyModelSelection = resolveDefaultableModelSelection( selectedEnvironmentServerConfig, storedStickyModelSelection, selectedProject?.id ?? null, ); const modelOptions = buildModelOptions( selectedEnvironmentServerConfig, draftModelSelection ?? projectDefaultModelSelection ?? stickyModelSelection, undefined, selectedProject?.id ?? null, ); const selectedModel = resolveNewTaskModelSelection({ draftSelection: draftModelSelection, projectDefaultSelection: projectDefaultModelSelection, stickySelection: stickyModelSelection, modelOptions, }); const selectedModelKey = selectedModel ? `${selectedModel.instanceId}:${selectedModel.model}` : null; const selectedModelOption = modelOptions.find( (option) => selectedModel && option.selection.instanceId === selectedModel.instanceId && option.selection.model === selectedModel.model, ) ?? null; const selectedProviderStatus = selectedEnvironmentServerConfig?.providers.find( (provider) => provider.instanceId === selectedModel?.instanceId, ) ?? null; const planModeEnabled = legacyPlanModeEnabled && selectedProviderStatus?.showInteractionModeToggle !== false; const interactionMode = planModeEnabled ? (selectedProjectDraft.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE) : DEFAULT_PROVIDER_INTERACTION_MODE; const setSelectedModelKey = ( key: string | null, options?: ReadonlyArray, ) => { if (!key || !selectedProjectDraftKey) { return; } const option = modelOptions.find((candidate) => candidate.key === key); if (!option) { return; } const selection = withRememberedModelOptions( options ? { ...option.selection, options } : option.selection, ); const provider = selectedEnvironmentServerConfig?.providers.find( (candidate) => candidate.instanceId === selection.instanceId, ); updateComposerDraftSettings(selectedProjectDraftKey, { modelSelection: selection, ...(provider?.showInteractionModeToggle === false ? { interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE } : {}), }); setStickyComposerModelSelection(selection); }; const setSelectedModelOptions = (options: ReadonlyArray | undefined) => { if (!selectedModel || !selectedProjectDraftKey) { return; } rememberModelOptions(selectedModel.instanceId, selectedModel.model, options ?? []); const nextSelection: ModelSelection = options ? { ...selectedModel, options } : { instanceId: selectedModel.instanceId, model: selectedModel.model, }; updateComposerDraftSettings(selectedProjectDraftKey, { modelSelection: nextSelection, }); setStickyComposerModelSelection(nextSelection); }; const providerGroups = groupByProvider(modelOptions); const setPrompt = (value: string) => { if (!selectedProjectDraftKey) { return; } setComposerDraftText(selectedProjectDraftKey, value); }; const replaceAttachments = (nextAttachments: ReadonlyArray) => { if (!selectedProjectDraftKey) { return; } replaceComposerDraftAttachments(selectedProjectDraftKey, nextAttachments); }; const appendAttachments = ( nextAttachments: ReadonlyArray, insertion?: ComposerDraftInsertion, ): number => { if (!selectedProjectDraftKey) { return 0; } return appendComposerDraftAttachments(selectedProjectDraftKey, nextAttachments, { appendReference: true, insertion, }); }; const removeAttachment = (imageId: string) => { if (!selectedProjectDraftKey) { return; } removeComposerDraftAttachment(selectedProjectDraftKey, imageId); }; const clearAttachments = () => { if (!selectedProjectDraftKey) { return; } replaceComposerDraftAttachments(selectedProjectDraftKey, []); }; const carryDraftContentTo = (project: EnvironmentProject): string => { const target = { environmentId: project.environmentId, projectId: project.id }; const current = readFlow().draftKey; if (current !== null && isNewTaskDraftKey(current)) { retargetNewTaskDraft(current, target); return current; } return createNewTaskDraft(target); }; const setProject = (project: EnvironmentProject) => { updateFlow({ draftKey: carryDraftContentTo(project), environmentId: project.environmentId, projectKey: scopedProjectKey(project.environmentId, project.id), }); }; const selectEnvironment = (environmentId: EnvironmentId) => { const match = resolveEnvironmentProjectMatch( projects.filter((project) => project.environmentId === environmentId), selectedProject, ); updateFlow({ draftKey: match ? carryDraftContentTo(match) : readFlow().draftKey, environmentId, projectKey: match ? scopedProjectKey(match.environmentId, match.id) : null, }); }; const openScratch = useAtomCommand(projectEnvironment.openScratch, { reportFailure: false }); const [switchingToEnvironmentId, setSwitchingToEnvironmentId] = useState( null, ); const latestSwitchRef = useRef(null); const switchEnvironment = async (environmentId: EnvironmentId): Promise => { if (environmentId === selectedEnvironmentId) { latestSwitchRef.current = null; setSwitchingToEnvironmentId(null); return true; } if (!isScratchDraft) { selectEnvironment(environmentId); return true; } const request = {}; latestSwitchRef.current = request; setSwitchingToEnvironmentId(environmentId); const result = await openScratch({ environmentId, input: {} }); if (latestSwitchRef.current !== request) return false; latestSwitchRef.current = null; setSwitchingToEnvironmentId(null); if (result._tag === "Success") { setProject(result.value); return true; } if (!isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); Dialog.alert( "Could not switch machine", error instanceof Error ? error.message : "The folder for threads without a project could not be created.", ); } return false; }; const canStartWithoutProject = !isScratchDraft && scratchEnvironments.some((environment) => environment.environmentId === selectedEnvironmentId); const scratchStartInFlightRef = useRef(false); // Moves the draft, and what is typed in it, to the selected machine's Scratch project. const startWithoutProject = async () => { if (!canStartWithoutProject || selectedEnvironmentId === null) return; if (scratchStartInFlightRef.current) return; scratchStartInFlightRef.current = true; const requestedProjectKey = readFlow().projectKey; const result = await openScratch({ environmentId: selectedEnvironmentId, input: {} }); scratchStartInFlightRef.current = false; if (result._tag === "Success") { if (readFlow().projectKey === requestedProjectKey) setProject(result.value); return; } if (!isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); Dialog.alert( "Could not start without a project", error instanceof Error ? error.message : "The folder for threads without a project could not be created.", ); } }; const setWorkspaceMode = (mode: WorkspaceMode) => { if (!selectedProjectDraftKey) { return; } if (!selectedProject) { return; } const localSelection = resolveNewTaskLocalWorkspaceSelection({ branches: availableBranches, projectCwd: selectedProject.workspaceRoot, }); updateComposerDraftSettings(selectedProjectDraftKey, { workspaceSelection: { mode, branch: mode === "local" ? localSelection.branch : selectedBranchName, worktreePath: mode === "local" ? localSelection.worktreePath : selectedWorktreePath, ...(draftStartFromOrigin !== undefined ? { startFromOrigin: draftStartFromOrigin } : {}), // Refs have not named the current branch yet; the selection follows it. ...(mode === "local" && localSelection.awaitsCurrentBranch ? { followsCurrentCheckout: true } : {}), }, }); }; const selectBranch = (branch: VcsRef) => { if (!selectedProject || !selectedProjectDraftKey) { return; } updateComposerDraftSettings(selectedProjectDraftKey, { workspaceSelection: { mode: workspaceMode, branch: branch.name, worktreePath: resolveNewTaskBranchWorktreePath({ workspaceMode, projectCwd: selectedProject.workspaceRoot, branchWorktreePath: branch.worktreePath, }), ...(draftStartFromOrigin !== undefined ? { startFromOrigin: draftStartFromOrigin } : {}), }, }); }; const setStartFromOrigin = (value: boolean) => { if (!selectedProjectDraftKey) { return; } updateComposerDraftSettings(selectedProjectDraftKey, { workspaceSelection: { mode: workspaceMode, branch: selectedBranchName, worktreePath: selectedWorktreePath, startFromOrigin: value, ...(selectedBranchName === null && workspaceSelection.followsCurrentCheckout ? { followsCurrentCheckout: true } : {}), }, }); }; const refreshBranches = branchState.refresh; const loadBranches = () => { if (!selectedProject) { return; } setPendingConnectionError(null); refreshBranches(); }; const setRuntimeMode = (value: RuntimeMode) => { if (selectedProjectDraftKey) { updateComposerDraftSettings(selectedProjectDraftKey, { runtimeMode: value }); } }; const setInteractionMode = (value: ProviderInteractionMode) => { if (selectedProjectDraftKey) { updateComposerDraftSettings(selectedProjectDraftKey, { interactionMode: resolveProviderInteractionMode(selectedProviderStatus, value), }); } }; const buildPendingTaskMessage = ( metadata: TurnCommandMetadata, options?: { readonly currentCheckoutBranch?: string | null }, ): QueuedThreadMessage | null => { if (!selectedProject || !selectedProjectDraftKey) { return null; } const draft = getComposerDraftSnapshot(selectedProjectDraftKey); const text = draft.text.trim(); const draftModelSelection = resolveSelectableModelSelection( selectedEnvironmentServerConfig, draft.modelSelection ?? null, selectedProject.id, ) ?? selectedModel; if ((text.length === 0 && draft.attachments.length === 0) || !draftModelSelection) { return null; } if ( threadContextsOutsideEnvironment({ text: draft.text, records: draft.context?.records ?? [], environmentId: selectedProject.environmentId, }).length > 0 ) { return null; } const workspaceSelection = resolveWorkspaceSelection(draft.workspaceSelection); const mode = workspaceSelection.mode; return { environmentId: selectedProject.environmentId, threadId: ThreadId.make(metadata.threadId), messageId: MessageId.make(metadata.messageId), commandId: CommandId.make(metadata.commandId), text, attachments: draft.attachments, context: draft.context, modelSelection: draftModelSelection, runtimeMode: draft.runtimeMode ?? defaultRuntimeMode, interactionMode: resolvePendingTaskInteractionMode({ planModeEnabled: legacyPlanModeEnabled, draftInteractionMode: draft.interactionMode, provider: selectedEnvironmentServerConfig?.providers.find( (candidate) => candidate.instanceId === draftModelSelection.instanceId, ), }), creation: { projectId: selectedProject.id, projectTitle: selectedProject.title, projectCwd: selectedProject.workspaceRoot, workspaceMode: mode, ...(mode === "worktree" && workspaceSelection?.pullRequestReference ? { pullRequestReference: workspaceSelection.pullRequestReference } : {}), // Queued local tasks use drain-time checkout; snapshot branches only for immediate sends or explicit choices. branch: resolveProjectThreadCreationBranch({ workspaceMode: mode, selectedBranch: workspaceSelection?.branch ?? null, currentCheckoutBranch: options?.currentCheckoutBranch ?? null, }), worktreePath: mode === "worktree" ? null : (workspaceSelection?.worktreePath ?? null), ...((workspaceSelection?.startFromOrigin ?? startFromOrigin) ? { startFromOrigin: true } : {}), }, createdAt: metadata.createdAt, }; }; const value = { activeNewTask, projectScopes, selectedEnvironmentId, selectedProjectKey, selectedModelKey, workspaceMode, isGitRepo, isGitRepoKnown: knownIsGitRepo !== null, canChooseWorkspace, selectedBranchName, selectedWorktreePath, resolveWorkspaceSelection, startFromOrigin, draftKey: selectedProjectDraftKey, pullRequestReference: selectedProjectDraft.workspaceSelection?.pullRequestReference, prompt, attachments, submitting, branchesLoading, availableBranches, currentCheckoutBranchName, runtimeMode, interactionMode, planModeEnabled, expandedProvider, environments, selectedProject, isScratchDraft, modelOptions, selectedModel, selectedModelOption, selectedProviderStatus, providerGroups, setProject, canStartWithoutProject, startWithoutProject, getSelectedProjectKey: () => readFlow().projectKey, missingRouteDraftId: routeDraftResolution.kind === "missing" ? (props.routeDraftId ?? null) : null, selectEnvironment, switchEnvironment, switchingToEnvironmentId, setSelectedModelKey, setWorkspaceMode, selectBranch, setStartFromOrigin, buildPendingTaskMessage, setPrompt, replaceAttachments, appendAttachments, removeAttachment, clearAttachments, setSubmitting: (value: boolean) => updateFlow({ submitting: value }), loadBranches, setRuntimeMode, setInteractionMode, setSelectedModelOptions, setExpandedProvider: (value: string | null) => updateFlow({ expandedProvider: value }), } satisfies NewTaskFlowContextValue; return ( {props.children} ); } export function useNewTaskFlow() { const value = React.use(NewTaskFlowContext); if (value === null) { throw new Error("useNewTaskFlow must be used within NewTaskFlowProvider."); } return value; }