1import { EnvironmentId, ProjectId } from "@t3tools/contracts";
2import * as Schema from "effect/Schema";
3
4export class ProjectThreadTaskRequiredError extends Schema.TaggedError<ProjectThreadTaskRequiredError>()(
5 "ProjectThreadTaskRequiredError",
6 {
7 environmentId: EnvironmentId,
8 projectId: ProjectId,
9 environmentMode: Schema.Literals(["local", "worktree"]),
10 },
11) {
12 override get message(): string {
13 return "Enter a task before starting the thread.";
14 }
15}
16
17export class ProjectThreadBaseBranchRequiredError extends Schema.TaggedError<ProjectThreadBaseBranchRequiredError>()(
18 "ProjectThreadBaseBranchRequiredError",
19 {
20 environmentId: EnvironmentId,
21 projectId: ProjectId,
22 },
23) {
24 override get message(): string {
25 return "Select a base branch before creating a worktree.";
26 }
27}
28
29export const ProjectThreadCreationValidationError = Schema.Union([
30 ProjectThreadTaskRequiredError,
31 ProjectThreadBaseBranchRequiredError,
32]);
33export type ProjectThreadCreationValidationError = typeof ProjectThreadCreationValidationError.Type;
34
35/**
36 * Branch recorded on a thread created from the new-task composer. An explicit
37 * picker choice always wins. An untouched current-checkout draft records the
38 * ref that is actually checked out, so the thread's PR badge and branch label
39 * match the live git status instead of staying blank. A detached HEAD, a
40 * non-repository project, or a status that has not arrived stays null rather
41 * than fabricating a branch. Only the online creation path resolves a
42 * checkout; a queued task cannot know the checkout it will drain against.
43 */
44export function resolveProjectThreadCreationBranch(input: {
45 readonly workspaceMode: "local" | "worktree";
46 readonly selectedBranch: string | null;
47 readonly currentCheckoutBranch: string | null;
48}): string | null {
49 if (input.selectedBranch !== null) {
50 return input.selectedBranch;
51 }
52 return input.workspaceMode === "local" ? input.currentCheckoutBranch : null;
53}
54
55export function validateProjectThreadCreation(input: {
56 readonly environmentId: EnvironmentId;
57 readonly projectId: ProjectId;
58 readonly environmentMode: "local" | "worktree";
59 readonly branch: string | null;
60 readonly initialMessageText: string;
61}): ProjectThreadCreationValidationError | null {
62 if (input.initialMessageText.trim().length === 0) {
63 return new ProjectThreadTaskRequiredError({
64 environmentId: input.environmentId,
65 projectId: input.projectId,
66 environmentMode: input.environmentMode,
67 });
68 }
69 if (input.environmentMode === "worktree" && !input.branch) {
70 return new ProjectThreadBaseBranchRequiredError({
71 environmentId: input.environmentId,
72 projectId: input.projectId,
73 });
74 }
75 return null;
76}