173 lines · 5.4 KB · TypeScript
RawDownload
import { ProjectId, type ProviderKind, type ThreadId } from "@t3tools/contracts";import { type ChatMessage, type Thread } from "../types";import { randomUUID } from "~/lib/utils";import { getAppModelOptions } from "../appSettings";import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore";import { Schema } from "effect";import {  filterTerminalContextsWithText,  stripInlineTerminalContextPlaceholders,  type TerminalContextDraft,} from "../lib/terminalContext"; export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project";const WORKTREE_BRANCH_PREFIX = "t3code"; export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String); export function buildLocalDraftThread(  threadId: ThreadId,  draftThread: DraftThreadState,  fallbackModel: string,  error: string | null,): Thread {  return {    id: threadId,    codexThreadId: null,    projectId: draftThread.projectId,    title: "New thread",    model: fallbackModel,    runtimeMode: draftThread.runtimeMode,    interactionMode: draftThread.interactionMode,    session: null,    messages: [],    error,    createdAt: draftThread.createdAt,    latestTurn: null,    lastVisitedAt: draftThread.createdAt,    branch: draftThread.branch,    worktreePath: draftThread.worktreePath,    turnDiffSummaries: [],    activities: [],    proposedPlans: [],  };} export function revokeBlobPreviewUrl(previewUrl: string | undefined): void {  if (!previewUrl || typeof URL === "undefined" || !previewUrl.startsWith("blob:")) {    return;  }  URL.revokeObjectURL(previewUrl);} export function revokeUserMessagePreviewUrls(message: ChatMessage): void {  if (message.role !== "user" || !message.attachments) {    return;  }  for (const attachment of message.attachments) {    if (attachment.type !== "image") {      continue;    }    revokeBlobPreviewUrl(attachment.previewUrl);  }} export function collectUserMessageBlobPreviewUrls(message: ChatMessage): string[] {  if (message.role !== "user" || !message.attachments) {    return [];  }  const previewUrls: string[] = [];  for (const attachment of message.attachments) {    if (attachment.type !== "image") continue;    if (!attachment.previewUrl || !attachment.previewUrl.startsWith("blob:")) continue;    previewUrls.push(attachment.previewUrl);  }  return previewUrls;} export type SendPhase = "idle" | "preparing-worktree" | "sending-turn"; export interface PullRequestDialogState {  initialReference: string | null;  key: number;} export function readFileAsDataUrl(file: File): Promise<string> {  return new Promise((resolve, reject) => {    const reader = new FileReader();    reader.addEventListener("load", () => {      if (typeof reader.result === "string") {        resolve(reader.result);        return;      }      reject(new Error("Could not read image data."));    });    reader.addEventListener("error", () => {      reject(reader.error ?? new Error("Failed to read image."));    });    reader.readAsDataURL(file);  });} export function buildTemporaryWorktreeBranchName(): string {  // Keep the 8-hex suffix shape for backend temporary-branch detection.  const token = randomUUID().slice(0, 8).toLowerCase();  return `${WORKTREE_BRANCH_PREFIX}/${token}`;} export function cloneComposerImageForRetry(  image: ComposerImageAttachment,): ComposerImageAttachment {  if (typeof URL === "undefined" || !image.previewUrl.startsWith("blob:")) {    return image;  }  try {    return {      ...image,      previewUrl: URL.createObjectURL(image.file),    };  } catch {    return image;  }} export function getCustomModelOptionsByProvider(settings: {  customCodexModels: readonly string[];  customClaudeModels: readonly string[];}): Record<ProviderKind, ReadonlyArray<{ slug: string; name: string }>> {  return {    codex: getAppModelOptions("codex", settings.customCodexModels),    claudeAgent: getAppModelOptions("claudeAgent", settings.customClaudeModels),  };} export function deriveComposerSendState(options: {  prompt: string;  imageCount: number;  terminalContexts: ReadonlyArray<TerminalContextDraft>;}): {  trimmedPrompt: string;  sendableTerminalContexts: TerminalContextDraft[];  expiredTerminalContextCount: number;  hasSendableContent: boolean;} {  const trimmedPrompt = stripInlineTerminalContextPlaceholders(options.prompt).trim();  const sendableTerminalContexts = filterTerminalContextsWithText(options.terminalContexts);  const expiredTerminalContextCount =    options.terminalContexts.length - sendableTerminalContexts.length;  return {    trimmedPrompt,    sendableTerminalContexts,    expiredTerminalContextCount,    hasSendableContent:      trimmedPrompt.length > 0 || options.imageCount > 0 || sendableTerminalContexts.length > 0,  };} export function buildExpiredTerminalContextToastCopy(  expiredTerminalContextCount: number,  variant: "omitted" | "empty",): { title: string; description: string } {  const count = Math.max(1, Math.floor(expiredTerminalContextCount));  const noun = count === 1 ? "Expired terminal context" : "Expired terminal contexts";  if (variant === "empty") {    return {      title: `${noun} won't be sent`,      description: "Remove it or re-add it to include terminal output.",    };  }  return {    title: `${noun} omitted from message`,    description: "Re-add it if you want that terminal output included.",  };}