1599 lines · 57.3 KB · TypeScript
RawDownload
import {  DEFAULT_REASONING_EFFORT_BY_PROVIDER,  ProjectId,  REASONING_EFFORT_OPTIONS_BY_PROVIDER,  ThreadId,  type CodexReasoningEffort,  type ProviderKind,  type ProviderInteractionMode,  type RuntimeMode,} from "@t3tools/contracts";import { normalizeModelSlug } from "@t3tools/shared/model";import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type ChatImageAttachment } from "./types";import {  type TerminalContextDraft,  ensureInlineTerminalContextPlaceholders,  normalizeTerminalContextText,} from "./lib/terminalContext";import { Debouncer } from "@tanstack/react-pacer";import { create } from "zustand";import { createJSONStorage, persist, type StateStorage } from "zustand/middleware"; export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";export type DraftThreadEnvMode = "local" | "worktree"; const COMPOSER_PERSIST_DEBOUNCE_MS = 300; interface DebouncedStorage extends StateStorage {  flush: () => void;} export function createDebouncedStorage(baseStorage: StateStorage): DebouncedStorage {  const debouncedSetItem = new Debouncer(    (name: string, value: string) => {      baseStorage.setItem(name, value);    },    { wait: COMPOSER_PERSIST_DEBOUNCE_MS },  );   return {    getItem: (name) => baseStorage.getItem(name),    setItem: (name, value) => {      debouncedSetItem.maybeExecute(name, value);    },    removeItem: (name) => {      debouncedSetItem.cancel();      baseStorage.removeItem(name);    },    flush: () => {      debouncedSetItem.flush();    },  };} const composerDebouncedStorage: DebouncedStorage =  typeof localStorage !== "undefined"    ? createDebouncedStorage(localStorage)    : { getItem: () => null, setItem: () => {}, removeItem: () => {}, flush: () => {} }; // Flush pending composer draft writes before page unload to prevent data loss.if (typeof window !== "undefined") {  window.addEventListener("beforeunload", () => {    composerDebouncedStorage.flush();  });} export interface PersistedComposerImageAttachment {  id: string;  name: string;  mimeType: string;  sizeBytes: number;  dataUrl: string;} export interface ComposerImageAttachment extends Omit<ChatImageAttachment, "previewUrl"> {  previewUrl: string;  file: File;} interface PersistedTerminalContextDraft {  id: string;  threadId: ThreadId;  createdAt: string;  terminalId: string;  terminalLabel: string;  lineStart: number;  lineEnd: number;} interface PersistedComposerThreadDraftState {  prompt: string;  attachments: PersistedComposerImageAttachment[];  terminalContexts?: PersistedTerminalContextDraft[];  provider?: ProviderKind | null;  model?: string | null;  runtimeMode?: RuntimeMode | null;  interactionMode?: ProviderInteractionMode | null;  effort?: CodexReasoningEffort | null;  codexFastMode?: boolean | null;  serviceTier?: string | null;} interface PersistedDraftThreadState {  projectId: ProjectId;  createdAt: string;  runtimeMode: RuntimeMode;  interactionMode: ProviderInteractionMode;  branch: string | null;  worktreePath: string | null;  envMode: DraftThreadEnvMode;} interface PersistedComposerDraftStoreState {  draftsByThreadId: Record<ThreadId, PersistedComposerThreadDraftState>;  draftThreadsByThreadId: Record<ThreadId, PersistedDraftThreadState>;  projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>;} interface ComposerThreadDraftState {  prompt: string;  images: ComposerImageAttachment[];  nonPersistedImageIds: string[];  persistedAttachments: PersistedComposerImageAttachment[];  terminalContexts: TerminalContextDraft[];  provider: ProviderKind | null;  model: string | null;  runtimeMode: RuntimeMode | null;  interactionMode: ProviderInteractionMode | null;  effort: CodexReasoningEffort | null;  codexFastMode: boolean;} export interface DraftThreadState {  projectId: ProjectId;  createdAt: string;  runtimeMode: RuntimeMode;  interactionMode: ProviderInteractionMode;  branch: string | null;  worktreePath: string | null;  envMode: DraftThreadEnvMode;} interface ProjectDraftThread extends DraftThreadState {  threadId: ThreadId;} interface ComposerDraftStoreState {  draftsByThreadId: Record<ThreadId, ComposerThreadDraftState>;  draftThreadsByThreadId: Record<ThreadId, DraftThreadState>;  projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>;  getDraftThreadByProjectId: (projectId: ProjectId) => ProjectDraftThread | null;  getDraftThread: (threadId: ThreadId) => DraftThreadState | null;  setProjectDraftThreadId: (    projectId: ProjectId,    threadId: ThreadId,    options?: {      branch?: string | null;      worktreePath?: string | null;      createdAt?: string;      envMode?: DraftThreadEnvMode;      runtimeMode?: RuntimeMode;      interactionMode?: ProviderInteractionMode;    },  ) => void;  setDraftThreadContext: (    threadId: ThreadId,    options: {      branch?: string | null;      worktreePath?: string | null;      projectId?: ProjectId;      createdAt?: string;      envMode?: DraftThreadEnvMode;      runtimeMode?: RuntimeMode;      interactionMode?: ProviderInteractionMode;    },  ) => void;  clearProjectDraftThreadId: (projectId: ProjectId) => void;  clearProjectDraftThreadById: (projectId: ProjectId, threadId: ThreadId) => void;  clearDraftThread: (threadId: ThreadId) => void;  setPrompt: (threadId: ThreadId, prompt: string) => void;  setTerminalContexts: (threadId: ThreadId, contexts: TerminalContextDraft[]) => void;  setProvider: (threadId: ThreadId, provider: ProviderKind | null | undefined) => void;  setModel: (threadId: ThreadId, model: string | null | undefined) => void;  setRuntimeMode: (threadId: ThreadId, runtimeMode: RuntimeMode | null | undefined) => void;  setInteractionMode: (    threadId: ThreadId,    interactionMode: ProviderInteractionMode | null | undefined,  ) => void;  setEffort: (threadId: ThreadId, effort: CodexReasoningEffort | null | undefined) => void;  setCodexFastMode: (threadId: ThreadId, enabled: boolean | null | undefined) => void;  addImage: (threadId: ThreadId, image: ComposerImageAttachment) => void;  addImages: (threadId: ThreadId, images: ComposerImageAttachment[]) => void;  removeImage: (threadId: ThreadId, imageId: string) => void;  insertTerminalContext: (    threadId: ThreadId,    prompt: string,    context: TerminalContextDraft,    index: number,  ) => boolean;  addTerminalContext: (threadId: ThreadId, context: TerminalContextDraft) => void;  addTerminalContexts: (threadId: ThreadId, contexts: TerminalContextDraft[]) => void;  removeTerminalContext: (threadId: ThreadId, contextId: string) => void;  clearTerminalContexts: (threadId: ThreadId) => void;  clearPersistedAttachments: (threadId: ThreadId) => void;  syncPersistedAttachments: (    threadId: ThreadId,    attachments: PersistedComposerImageAttachment[],  ) => void;  clearComposerContent: (threadId: ThreadId) => void;  clearThreadDraft: (threadId: ThreadId) => void;} const EMPTY_PERSISTED_DRAFT_STORE_STATE: PersistedComposerDraftStoreState = {  draftsByThreadId: {},  draftThreadsByThreadId: {},  projectDraftThreadIdByProjectId: {},}; const EMPTY_IMAGES: ComposerImageAttachment[] = [];const EMPTY_IDS: string[] = [];const EMPTY_PERSISTED_ATTACHMENTS: PersistedComposerImageAttachment[] = [];const EMPTY_TERMINAL_CONTEXTS: TerminalContextDraft[] = [];Object.freeze(EMPTY_IMAGES);Object.freeze(EMPTY_IDS);Object.freeze(EMPTY_PERSISTED_ATTACHMENTS);Object.freeze(EMPTY_TERMINAL_CONTEXTS);const EMPTY_THREAD_DRAFT = Object.freeze({  prompt: "",  images: EMPTY_IMAGES,  nonPersistedImageIds: EMPTY_IDS,  persistedAttachments: EMPTY_PERSISTED_ATTACHMENTS,  terminalContexts: EMPTY_TERMINAL_CONTEXTS,  provider: null,  model: null,  runtimeMode: null,  interactionMode: null,  effort: null,  codexFastMode: false,}) as ComposerThreadDraftState; const REASONING_EFFORT_VALUES = new Set<CodexReasoningEffort>(  REASONING_EFFORT_OPTIONS_BY_PROVIDER.codex,); function createEmptyThreadDraft(): ComposerThreadDraftState {  return {    prompt: "",    images: [],    nonPersistedImageIds: [],    persistedAttachments: [],    terminalContexts: [],    provider: null,    model: null,    runtimeMode: null,    interactionMode: null,    effort: null,    codexFastMode: false,  };} function composerImageDedupKey(image: ComposerImageAttachment): string {  // Keep this independent from File.lastModified so dedupe is stable for hydrated  // images reconstructed from localStorage (which get a fresh lastModified value).  return `${image.mimeType}\u0000${image.sizeBytes}\u0000${image.name}`;} function terminalContextDedupKey(context: TerminalContextDraft): string {  return `${context.terminalId}\u0000${context.lineStart}\u0000${context.lineEnd}`;} function normalizeTerminalContextForThread(  threadId: ThreadId,  context: TerminalContextDraft,): TerminalContextDraft | null {  const terminalId = context.terminalId.trim();  const terminalLabel = context.terminalLabel.trim();  if (terminalId.length === 0 || terminalLabel.length === 0) {    return null;  }  const lineStart = Math.max(1, Math.floor(context.lineStart));  const lineEnd = Math.max(lineStart, Math.floor(context.lineEnd));  return {    ...context,    threadId,    terminalId,    terminalLabel,    lineStart,    lineEnd,    text: normalizeTerminalContextText(context.text),  };} function normalizeTerminalContextsForThread(  threadId: ThreadId,  contexts: ReadonlyArray<TerminalContextDraft>,): TerminalContextDraft[] {  const existingIds = new Set<string>();  const existingDedupKeys = new Set<string>();  const normalizedContexts: TerminalContextDraft[] = [];   for (const context of contexts) {    const normalizedContext = normalizeTerminalContextForThread(threadId, context);    if (!normalizedContext) {      continue;    }    const dedupKey = terminalContextDedupKey(normalizedContext);    if (existingIds.has(normalizedContext.id) || existingDedupKeys.has(dedupKey)) {      continue;    }    normalizedContexts.push(normalizedContext);    existingIds.add(normalizedContext.id);    existingDedupKeys.add(dedupKey);  }   return normalizedContexts;} function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean {  return (    draft.prompt.length === 0 &&    draft.images.length === 0 &&    draft.persistedAttachments.length === 0 &&    draft.terminalContexts.length === 0 &&    draft.provider === null &&    draft.model === null &&    draft.runtimeMode === null &&    draft.interactionMode === null &&    draft.effort === null &&    draft.codexFastMode === false  );} function normalizeProviderKind(value: unknown): ProviderKind | null {  return value === "codex" ? value : null;} function revokeObjectPreviewUrl(previewUrl: string): void {  if (typeof URL === "undefined") {    return;  }  if (!previewUrl.startsWith("blob:")) {    return;  }  URL.revokeObjectURL(previewUrl);} function normalizePersistedAttachment(value: unknown): PersistedComposerImageAttachment | null {  if (!value || typeof value !== "object") {    return null;  }  const candidate = value as Record<string, unknown>;  const id = candidate.id;  const name = candidate.name;  const mimeType = candidate.mimeType;  const sizeBytes = candidate.sizeBytes;  const dataUrl = candidate.dataUrl;  if (    typeof id !== "string" ||    typeof name !== "string" ||    typeof mimeType !== "string" ||    typeof sizeBytes !== "number" ||    !Number.isFinite(sizeBytes) ||    typeof dataUrl !== "string" ||    id.length === 0 ||    dataUrl.length === 0  ) {    return null;  }  return {    id,    name,    mimeType,    sizeBytes,    dataUrl,  };} function normalizePersistedTerminalContextDraft(  value: unknown,): PersistedTerminalContextDraft | null {  if (!value || typeof value !== "object") {    return null;  }  const candidate = value as Record<string, unknown>;  const id = candidate.id;  const threadId = candidate.threadId;  const createdAt = candidate.createdAt;  const lineStart = candidate.lineStart;  const lineEnd = candidate.lineEnd;  if (    typeof id !== "string" ||    id.length === 0 ||    typeof threadId !== "string" ||    threadId.length === 0 ||    typeof createdAt !== "string" ||    createdAt.length === 0 ||    typeof lineStart !== "number" ||    !Number.isFinite(lineStart) ||    typeof lineEnd !== "number" ||    !Number.isFinite(lineEnd)  ) {    return null;  }  const terminalId = typeof candidate.terminalId === "string" ? candidate.terminalId.trim() : "";  const terminalLabel =    typeof candidate.terminalLabel === "string" ? candidate.terminalLabel.trim() : "";  if (terminalId.length === 0 || terminalLabel.length === 0) {    return null;  }  const normalizedLineStart = Math.max(1, Math.floor(lineStart));  const normalizedLineEnd = Math.max(normalizedLineStart, Math.floor(lineEnd));  return {    id,    threadId: threadId as ThreadId,    createdAt,    terminalId,    terminalLabel,    lineStart: normalizedLineStart,    lineEnd: normalizedLineEnd,  };} function normalizeDraftThreadEnvMode(  value: unknown,  fallbackWorktreePath: string | null,): DraftThreadEnvMode {  if (value === "local" || value === "worktree") {    return value;  }  return fallbackWorktreePath ? "worktree" : "local";} function normalizePersistedComposerDraftState(value: unknown): PersistedComposerDraftStoreState {  if (!value || typeof value !== "object") {    return EMPTY_PERSISTED_DRAFT_STORE_STATE;  }  const candidate = value as Record<string, unknown>;  const rawDraftMap = candidate.draftsByThreadId;  const rawDraftThreadsByThreadId = candidate.draftThreadsByThreadId;  const rawProjectDraftThreadIdByProjectId = candidate.projectDraftThreadIdByProjectId;  const draftThreadsByThreadId: PersistedComposerDraftStoreState["draftThreadsByThreadId"] = {};  if (rawDraftThreadsByThreadId && typeof rawDraftThreadsByThreadId === "object") {    for (const [threadId, rawDraftThread] of Object.entries(      rawDraftThreadsByThreadId as Record<string, unknown>,    )) {      if (typeof threadId !== "string" || threadId.length === 0) {        continue;      }      if (!rawDraftThread || typeof rawDraftThread !== "object") {        continue;      }      const candidateDraftThread = rawDraftThread as Record<string, unknown>;      const projectId = candidateDraftThread.projectId;      const createdAt = candidateDraftThread.createdAt;      const branch = candidateDraftThread.branch;      const worktreePath = candidateDraftThread.worktreePath;      const normalizedWorktreePath = typeof worktreePath === "string" ? worktreePath : null;      if (typeof projectId !== "string" || projectId.length === 0) {        continue;      }      draftThreadsByThreadId[threadId as ThreadId] = {        projectId: projectId as ProjectId,        createdAt:          typeof createdAt === "string" && createdAt.length > 0            ? createdAt            : new Date().toISOString(),        runtimeMode:          candidateDraftThread.runtimeMode === "approval-required" ||          candidateDraftThread.runtimeMode === "full-access"            ? candidateDraftThread.runtimeMode            : DEFAULT_RUNTIME_MODE,        interactionMode:          candidateDraftThread.interactionMode === "plan" ||          candidateDraftThread.interactionMode === "default"            ? candidateDraftThread.interactionMode            : DEFAULT_INTERACTION_MODE,        branch: typeof branch === "string" ? branch : null,        worktreePath: normalizedWorktreePath,        envMode: normalizeDraftThreadEnvMode(candidateDraftThread.envMode, normalizedWorktreePath),      };    }  }  const projectDraftThreadIdByProjectId: PersistedComposerDraftStoreState["projectDraftThreadIdByProjectId"] =    {};  if (    rawProjectDraftThreadIdByProjectId &&    typeof rawProjectDraftThreadIdByProjectId === "object"  ) {    for (const [projectId, threadId] of Object.entries(      rawProjectDraftThreadIdByProjectId as Record<string, unknown>,    )) {      if (        typeof projectId === "string" &&        projectId.length > 0 &&        typeof threadId === "string" &&        threadId.length > 0      ) {        projectDraftThreadIdByProjectId[projectId as ProjectId] = threadId as ThreadId;        if (!draftThreadsByThreadId[threadId as ThreadId]) {          draftThreadsByThreadId[threadId as ThreadId] = {            projectId: projectId as ProjectId,

Lines 1–500 of 1599.