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";import { validateProjectThreadCreation } from "./projectThreadCreationValidation"; 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<DraftComposerImageAttachment>; }) => { const metadata = makeTurnCommandMetadata(); const threadId = ThreadId.make(metadata.threadId); const initialMessageText = input.initialMessageText.trim(); const nextTitle = deriveThreadTitleFromPrompt(input.initialMessageText); const validationError = validateProjectThreadCreation({ environmentId: input.project.environmentId, projectId: input.project.id, environmentMode: input.envMode, branch: input.branch, initialMessageText, }); if (validationError !== null) { setPendingConnectionError(validationError.message); return AsyncResult.failure(Cause.fail(validationError)); } 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], );}
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";
import { validateProjectThreadCreation } from "./projectThreadCreationValidation";
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<DraftComposerImageAttachment>;
}) => {
const metadata = makeTurnCommandMetadata();
const threadId = ThreadId.make(metadata.threadId);
const initialMessageText = input.initialMessageText.trim();
const nextTitle = deriveThreadTitleFromPrompt(input.initialMessageText);
const validationError = validateProjectThreadCreation({
environmentId: input.project.environmentId,
projectId: input.project.id,
environmentMode: input.envMode,
branch: input.branch,
initialMessageText,
});
if (validationError !== null) {
setPendingConnectionError(validationError.message);
return AsyncResult.failure(Cause.fail(validationError));
}
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],
);
}