Julius MarmingeT3 Code Mobile [WIP] (#2013)b3e8c035 months agoHistory
262 lines · 8.1 KB · TypeScript
RawDownload
import { useCallback } from "react"; import { EnvironmentScopedProjectShell, type VcsRef } from "@t3tools/client-runtime";import {  CommandId,  DEFAULT_PROVIDER_INTERACTION_MODE,  DEFAULT_RUNTIME_MODE,  type EnvironmentId,  MessageId,  ThreadId,  type ModelSelection,  type ProviderInteractionMode,  type RuntimeMode,} from "@t3tools/contracts";import { buildTemporaryWorktreeBranchName, sanitizeFeatureBranchName } from "@t3tools/shared/git";import { uuidv4 } from "../../lib/uuid"; import type { DraftComposerImageAttachment } from "../../lib/composerImages";import { makeTurnCommandMetadata } from "../../lib/commandMetadata";import { getEnvironmentClient } from "../../state/environment-session-registry";import { environmentRuntimeManager } from "../../state/use-environment-runtime";import { vcsRefManager } from "../../state/use-vcs-refs";import { useRemoteCatalog } from "../../state/use-remote-catalog";import {  setPendingConnectionError,  useRemoteEnvironmentState,} from "../../state/use-remote-environment-registry"; function useRefreshRemoteData() {  const { savedConnectionsById } = useRemoteEnvironmentState();   return useCallback(    async (environmentIds?: ReadonlyArray<EnvironmentId>) => {      const targets =        environmentIds ??        Object.values(savedConnectionsById).map((connection) => connection.environmentId);       await Promise.all(        targets.map(async (environmentId) => {          const client = getEnvironmentClient(environmentId);          if (!client) {            return;          }           try {            const serverConfig = await client.server.getConfig();            environmentRuntimeManager.patch({ environmentId }, (current) => ({              ...current,              serverConfig,              connectionError: null,            }));          } catch (error) {            environmentRuntimeManager.patch({ environmentId }, (current) => ({              ...current,              connectionError:                error instanceof Error ? error.message : "Failed to refresh remote data.",            }));          }        }),      );    },    [savedConnectionsById],  );} 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 useProjectActions() {  const { threads } = useRemoteCatalog();  const refreshRemoteData = useRefreshRemoteData();   const onCreateThreadWithOptions = useCallback(    async (input: {      readonly project: EnvironmentScopedProjectShell;      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 client = getEnvironmentClient(input.project.environmentId);      if (!client) {        return null;      }       const metadata = makeTurnCommandMetadata();      const threadId = ThreadId.make(metadata.threadId);      const createdAt = metadata.createdAt;      const initialMessageText = input.initialMessageText.trim();      const nextTitle = deriveThreadTitleFromPrompt(input.initialMessageText);       if (initialMessageText.length === 0) {        return null;      }      if (input.envMode === "worktree" && !input.branch) {        return null;      }       const isWorktree = input.envMode === "worktree";       await client.orchestration.dispatchCommand({        type: "thread.turn.start",        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,          },          ...(isWorktree            ? {                prepareWorktree: {                  projectCwd: input.project.workspaceRoot,                  baseBranch: input.branch!,                  branch: buildTemporaryWorktreeBranchName(uuidv4),                },                runSetupScript: true,              }            : {}),        },        createdAt,      });       await refreshRemoteData([input.project.environmentId]);      return {        environmentId: input.project.environmentId,        threadId,      };    },    [refreshRemoteData],  );   const onCreateThread = useCallback(    async (project: EnvironmentScopedProjectShell) => {      const latestProjectThread =        threads.find(          (thread) =>            thread.environmentId === project.environmentId && thread.projectId === project.id,        ) ?? null;      const modelSelection =        project.defaultModelSelection ?? latestProjectThread?.modelSelection ?? null;      if (!modelSelection) {        setPendingConnectionError("This project does not have a default model configured yet.");        return null;      }       return await onCreateThreadWithOptions({        project,        modelSelection,        envMode: "local",        branch: null,        worktreePath: null,        runtimeMode: DEFAULT_RUNTIME_MODE,        interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,        initialMessageText: "",        initialAttachments: [],      });    },    [onCreateThreadWithOptions, threads],  );   const onListProjectBranches = useCallback(    async (project: EnvironmentScopedProjectShell): Promise<ReadonlyArray<VcsRef>> => {      const client = getEnvironmentClient(project.environmentId);      if (!client) {        return [];      }       try {        const result = await vcsRefManager.load(          { environmentId: project.environmentId, cwd: project.workspaceRoot, query: null },          client.vcs,          { limit: 100 },        );        return (result?.refs ?? []).filter((branch) => !branch.isRemote);      } catch (error) {        setPendingConnectionError(          error instanceof Error ? error.message : "Failed to load branches.",        );        return [];      }    },    [],  );   const onCreateProjectWorktree = useCallback(    async (      project: EnvironmentScopedProjectShell,      nextWorktree: {        readonly baseBranch: string;        readonly newBranch: string;      },    ): Promise<{      readonly branch: string;      readonly worktreePath: string;    } | null> => {      const client = getEnvironmentClient(project.environmentId);      if (!client) {        return null;      }       try {        const result = await client.vcs.createWorktree({          cwd: project.workspaceRoot,          refName: nextWorktree.baseBranch,          newRefName: sanitizeFeatureBranchName(nextWorktree.newBranch),          path: null,        });        vcsRefManager.invalidate({          environmentId: project.environmentId,          cwd: project.workspaceRoot,          query: null,        });        return {          branch: result.worktree.refName,          worktreePath: result.worktree.path,        };      } catch (error) {        setPendingConnectionError(          error instanceof Error ? error.message : "Failed to create worktree.",        );        return null;      }    },    [],  );   return {    onCreateThread,    onCreateThreadWithOptions,    onListProjectBranches,    onCreateProjectWorktree,    onRefreshProjects: refreshRemoteData,  };}