Files
kandev/apps/web/components/task/new-subtask-form-state.ts
T
2026-05-16 20:07:57 +01:00

169 lines
6.0 KiB
TypeScript

"use client";
import { useMemo, useRef, useState } from "react";
import type { Branch, LocalRepository } from "@/lib/types/http";
import type {
DialogFormState,
StepType,
TaskFormInputsHandle,
} from "@/components/task-create-dialog-types";
import { useRepositoriesState } from "@/components/task-create-dialog-repositories-state";
/**
* Workspace mode the New Subtask dialog supports today. The shared_group
* mode is part of the office task-handoffs spec but only office agents
* surface it via MCP; the Kanban dialog covers the inherit_parent /
* new_workspace toggle (handoffs phase 5).
*/
export type SubtaskWorkspaceMode = "inherit_parent" | "new_workspace";
/**
* Default workspace mode for the New Subtask dialog. When the parent
* task has an active worktree we default to inherit_parent so the
* subtask runs in the same materialized environment without forcing
* the user through the repo picker; otherwise (parent has no
* materialized workspace yet) we default to new_workspace.
*/
export function defaultSubtaskWorkspaceMode(
parentWorktreeBranch: string | null,
): SubtaskWorkspaceMode {
return parentWorktreeBranch ? "inherit_parent" : "new_workspace";
}
/**
* Returns a `DialogFormState`-shaped object for the New Subtask dialog so it
* can reuse the create-task dialog's `RepoChipsRow`, `useDialogHandlers`, and
* `useGitHubUrlBranchesEffect` without any forking of those components.
*
* The subtask flow only exercises a slice of the full state: repo rows,
* GitHub URL mode, agent/executor profiles. The remaining fields (title,
* workflow, draft, fresh-branch, discovered repos) are kept as inert stubs
* because the subtask dialog renders its own title input and inherits the
* parent's workflow.
*/
export function useSubtaskFormState(): DialogFormState {
const repos = useRepositoriesState();
const [agentProfileId, setAgentProfileId] = useState("");
const [executorProfileId, setExecutorProfileId] = useState("");
const [useGitHubUrl, setUseGitHubUrl] = useState(false);
const [githubUrl, setGitHubUrl] = useState("");
const [githubBranch, setGitHubBranch] = useState("");
const [githubBranches, setGitHubBranches] = useState<Branch[]>([]);
const [githubBranchesLoading, setGitHubBranchesLoading] = useState(false);
const [githubUrlError, setGitHubUrlError] = useState<string | null>(null);
const [githubPrHeadBranch, setGitHubPrHeadBranch] = useState<string | null>(null);
// Discovered (on-disk) repos — populated by useDiscoverReposEffect when the
// dialog opens, same as the create-task flow. This lets users target
// not-yet-imported on-machine git folders for the subtask.
const [discoveredRepositories, setDiscoveredRepositories] = useState<LocalRepository[]>([]);
const [discoverReposLoading, setDiscoverReposLoading] = useState(false);
const [discoverReposLoaded, setDiscoverReposLoaded] = useState(false);
const descriptionInputRef = useRef<TaskFormInputsHandle | null>(null);
return useMemo<DialogFormState>(
() => ({
...INERT_TITLE_DRAFT,
currentDefaults: EMPTY_DEFAULTS,
descriptionInputRef,
// Repo chip row — what RepoChipsRow + useDialogHandlers actually drive.
repositories: repos.repositories,
setRepositories: repos.setRepositories,
addRepository: repos.addRepository,
removeRepository: repos.removeRepository,
updateRepository: repos.updateRepository,
githubBranch,
setGitHubBranch,
agentProfileId,
setAgentProfileId,
executorId: "",
setExecutorId: NOOP,
executorProfileId,
setExecutorProfileId,
discoveredRepositories,
setDiscoveredRepositories,
discoverReposLoading,
setDiscoverReposLoading,
discoverReposLoaded,
setDiscoverReposLoaded,
// Subtasks inherit the parent's workflow; no selector is rendered.
selectedWorkflowId: null,
setSelectedWorkflowId: NOOP,
fetchedSteps: EMPTY_STEPS,
setFetchedSteps: NOOP,
isCreatingSession: false,
setIsCreatingSession: NOOP,
isCreatingTask: false,
setIsCreatingTask: NOOP,
useGitHubUrl,
setUseGitHubUrl,
githubUrl,
setGitHubUrl,
githubBranches,
setGitHubBranches,
githubBranchesLoading,
setGitHubBranchesLoading,
githubUrlError,
setGitHubUrlError,
githubPrHeadBranch,
setGitHubPrHeadBranch,
workflowAgentProfileId: "",
setWorkflowAgentProfileId: NOOP,
clearDraft: NOOP,
...INERT_FRESH_BRANCH_AND_NOREPO,
}),
[
repos.repositories,
repos.setRepositories,
repos.addRepository,
repos.removeRepository,
repos.updateRepository,
githubBranch,
agentProfileId,
executorProfileId,
useGitHubUrl,
githubUrl,
githubBranches,
githubBranchesLoading,
githubUrlError,
githubPrHeadBranch,
discoveredRepositories,
discoverReposLoading,
discoverReposLoaded,
],
);
}
const NOOP = () => undefined;
const EMPTY_DEFAULTS = { name: "", description: "" };
const EMPTY_STEPS: StepType[] | null = null;
// Title / draft / openCycle are inert in the subtask flow — the dialog renders
// its own title input directly and doesn't restore drafts. Extracted so the
// useMemo body stays under the function-length lint cap.
const INERT_TITLE_DRAFT = {
taskName: "",
setTaskName: NOOP,
hasTitle: false,
setHasTitle: NOOP,
hasDescription: false,
setHasDescription: NOOP,
draftDescription: "",
openCycle: 0,
} as const;
// Fresh-branch (local-executor-only) and no-repo / scratch workspace mode are
// top-level create-task features; subtasks inherit their parent's repo
// context, so these are kept inert.
const INERT_FRESH_BRANCH_AND_NOREPO = {
freshBranchEnabled: false,
setFreshBranchEnabled: NOOP,
currentLocalBranch: "",
setCurrentLocalBranch: NOOP,
currentLocalBranchLoading: false,
setCurrentLocalBranchLoading: NOOP,
noRepository: false,
setNoRepository: NOOP,
workspacePath: "",
setWorkspacePath: NOOP,
} as const;