import { useCallback } from "react"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; import { mapAtomCommandResult } from "@t3tools/client-runtime/state/runtime"; import { CommandId, MessageId, ThreadId, type ModelSelection, type ProviderInteractionMode, type RuntimeMode, } from "@t3tools/contracts"; import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; import * as Cause from "effect/Cause"; import { AsyncResult } from "effect/unstable/reactivity"; import { threadEnvironment } from "../../state/threads"; import type { DraftComposerImageAttachment } from "../../lib/composerImages"; import { makeTurnCommandMetadata } from "../../lib/commandMetadata"; import { uuidv4 } from "../../lib/uuid"; import { useAtomCommand } from "../../state/use-atom-command"; import { setPendingConnectionError } from "../../state/use-remote-environment-registry"; function deriveThreadTitleFromPrompt(value: string): string { const trimmed = value.trim(); if (trimmed.length === 0) { return "New thread"; } const compact = trimmed.replace(/\s+/g, " "); return compact.length <= 72 ? compact : `${compact.slice(0, 69).trimEnd()}...`; } export function useCreateProjectThread() { const startTurn = useAtomCommand(threadEnvironment.startTurn, { reportFailure: false }); return useCallback( async (input: { readonly project: EnvironmentProject; readonly modelSelection: ModelSelection; readonly envMode: "local" | "worktree"; readonly branch: string | null; readonly worktreePath: string | null; readonly runtimeMode: RuntimeMode; readonly interactionMode: ProviderInteractionMode; readonly initialMessageText: string; readonly initialAttachments: ReadonlyArray; }) => { const metadata = makeTurnCommandMetadata(); const threadId = ThreadId.make(metadata.threadId); const initialMessageText = input.initialMessageText.trim(); const nextTitle = deriveThreadTitleFromPrompt(input.initialMessageText); if (initialMessageText.length === 0) { const error = new Error("Enter a task before starting the thread."); setPendingConnectionError(error.message); return AsyncResult.failure(Cause.fail(error)); } if (input.envMode === "worktree" && !input.branch) { const error = new Error("Select a base branch before creating a worktree."); setPendingConnectionError(error.message); return AsyncResult.failure(Cause.fail(error)); } const isWorktree = input.envMode === "worktree"; const result = await startTurn({ environmentId: input.project.environmentId, input: { commandId: CommandId.make(metadata.commandId), threadId, message: { messageId: MessageId.make(metadata.messageId), role: "user", text: initialMessageText, attachments: input.initialAttachments, }, modelSelection: input.modelSelection, titleSeed: nextTitle, runtimeMode: input.runtimeMode, interactionMode: input.interactionMode, bootstrap: { createThread: { projectId: input.project.id, title: nextTitle, modelSelection: input.modelSelection, runtimeMode: input.runtimeMode, interactionMode: input.interactionMode, branch: input.branch, worktreePath: isWorktree ? null : input.worktreePath, createdAt: metadata.createdAt, }, ...(isWorktree ? { prepareWorktree: { projectCwd: input.project.workspaceRoot, baseBranch: input.branch!, branch: buildTemporaryWorktreeBranchName(uuidv4), }, runSetupScript: true, } : {}), }, createdAt: metadata.createdAt, }, }); if (AsyncResult.isFailure(result)) { const error = Cause.squash(result.cause); setPendingConnectionError( error instanceof Error ? error.message : "The task could not be started.", ); return AsyncResult.failure(result.cause); } setPendingConnectionError(null); return mapAtomCommandResult(result, () => scopeThreadRef(input.project.environmentId, threadId), ); }, [startTurn], ); }