153 lines · 6.6 KB · TypeScript
RawDownload
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 {  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 { DraftComposerAttachment } from "../../lib/composerImages";import { prepareTurnAttachments, validateDraftFileAttachments } from "../../lib/attachmentUpload";import { makeTurnCommandMetadata, type TurnCommandMetadata } from "../../lib/commandMetadata";import { buildProjectThreadStartTurnInput } from "../../lib/projectThreadStartTurn";import { randomHex } from "../../lib/uuid";import { useAtomCommand } from "../../state/use-atom-command";import { scheduleUnusedComposerAttachmentCleanup } from "../../state/use-composer-drafts";import { setPendingConnectionError } from "../../state/use-remote-environment-registry";import { validateProjectThreadCreation } from "./projectThreadCreationValidation";import { appAtomRegistry } from "../../state/atom-registry";import { serverEnvironment } from "../../state/server"; 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 startFromOrigin?: boolean;      readonly runtimeMode: RuntimeMode;      readonly interactionMode: ProviderInteractionMode;      readonly initialMessageText: string;      readonly initialAttachments: ReadonlyArray<DraftComposerAttachment>;      readonly onAttachmentsUploaded: (        attachments: ReadonlyArray<DraftComposerAttachment>,      ) => Promise<void>;      /** Reuse identifiers from a queued pending task instead of minting new ones. */      readonly turnMetadata?: TurnCommandMetadata;    }) => {      const metadata = input.turnMetadata ?? makeTurnCommandMetadata();      const threadId = ThreadId.make(metadata.threadId);      const initialMessageText = input.initialMessageText.trim();       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 validateLiveFileAttachments = (        attachments: ReadonlyArray<DraftComposerAttachment>,      ): string | null =>        validateDraftFileAttachments({          attachments,          serverConfig: appAtomRegistry.get(            serverEnvironment.configValueAtom(input.project.environmentId),          ),        });      const initialAttachmentError = validateLiveFileAttachments(input.initialAttachments);      if (initialAttachmentError !== null) {        setPendingConnectionError(initialAttachmentError);        return AsyncResult.failure(Cause.fail(new Error(initialAttachmentError)));      }       let prepared: Awaited<ReturnType<typeof prepareTurnAttachments>>;      try {        // If persisting the references into the draft throws, the owner call        // deletes the pending uploads it minted before rethrowing.        prepared = await prepareTurnAttachments({          environmentId: input.project.environmentId,          attachments: input.initialAttachments,          persistUploadedReferences: async (draftAttachments) => {            await input.onAttachmentsUploaded(draftAttachments);            return "persisted";          },        });      } catch (error) {        const message = error instanceof Error ? error.message : "An attachment could not upload.";        setPendingConnectionError(message);        return AsyncResult.failure(Cause.fail(new Error(message)));      }      if (prepared.status !== "ready") {        const message = "The attachments are no longer available.";        setPendingConnectionError(message);        return AsyncResult.failure(Cause.fail(new Error(message)));      }       const preparedAttachmentError = validateLiveFileAttachments(prepared.draftAttachments);      if (preparedAttachmentError !== null) {        setPendingConnectionError(preparedAttachmentError);        return AsyncResult.failure(Cause.fail(new Error(preparedAttachmentError)));      }       const result = await startTurn({        environmentId: input.project.environmentId,        input: buildProjectThreadStartTurnInput({          projectId: input.project.id,          projectCwd: input.project.workspaceRoot,          threadId: metadata.threadId,          commandId: metadata.commandId,          messageId: metadata.messageId,          createdAt: metadata.createdAt,          text: initialMessageText,          attachments: input.initialAttachments,          uploadedAttachments: prepared.attachments,          modelSelection: input.modelSelection,          runtimeMode: input.runtimeMode,          interactionMode: input.interactionMode,          workspaceMode: input.envMode,          branch: input.branch,          worktreePath: input.worktreePath,          startFromOrigin: input.startFromOrigin ?? false,          worktreeBranchName: buildTemporaryWorktreeBranchName(randomHex),        }),      });      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);      }      // The started turn holds its own copy of the bytes; a failed delete is      // surfaced without failing the started task.      await prepared.releaseUploads().catch((error) => {        console.warn("[project-thread] could not delete consumed pending uploads", error);      });      setPendingConnectionError(null);      scheduleUnusedComposerAttachmentCleanup(input.initialAttachments);       return mapAtomCommandResult(result, () =>        scopeThreadRef(input.project.environmentId, threadId),      );    },    [startTurn],  );}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t