2212 lines · 79.6 KB · TypeScript
RawDownload
import {  CODEX_REASONING_EFFORT_OPTIONS,  type ClaudeCodeEffort,  type CodexReasoningEffort,  DEFAULT_MODEL_BY_PROVIDER,  ModelSelection,  ProjectId,  ProviderInteractionMode,  ProviderKind,  ProviderModelOptions,  RuntimeMode,  type ServerProvider,  ThreadId,} from "@t3tools/contracts";import * as Schema from "effect/Schema";import * as Equal from "effect/Equal";import { DeepMutable } from "effect/Types";import { normalizeModelSlug } from "@t3tools/shared/model";import { useMemo } from "react";import { getLocalStorageItem } from "./hooks/useLocalStorage";import { resolveAppModelSelection } from "./modelSelection";import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type ChatImageAttachment } from "./types";import {  type TerminalContextDraft,  ensureInlineTerminalContextPlaceholders,  normalizeTerminalContextText,} from "./lib/terminalContext";import { create } from "zustand";import { createJSONStorage, persist } from "zustand/middleware";import { createDebouncedStorage, createMemoryStorage } from "./lib/storage";import { getDefaultServerModel } from "./providerModels";import { UnifiedSettings } from "@t3tools/contracts/settings"; export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";const COMPOSER_DRAFT_STORAGE_VERSION = 3;const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]);export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type; const COMPOSER_PERSIST_DEBOUNCE_MS = 300; const composerDebouncedStorage = createDebouncedStorage(  typeof localStorage !== "undefined" ? localStorage : createMemoryStorage(),  COMPOSER_PERSIST_DEBOUNCE_MS,); // Flush pending composer draft writes before page unload to prevent data loss.if (typeof window !== "undefined") {  window.addEventListener("beforeunload", () => {    composerDebouncedStorage.flush();  });} export const PersistedComposerImageAttachment = Schema.Struct({  id: Schema.String,  name: Schema.String,  mimeType: Schema.String,  sizeBytes: Schema.Number,  dataUrl: Schema.String,});export type PersistedComposerImageAttachment = typeof PersistedComposerImageAttachment.Type; export interface ComposerImageAttachment extends Omit<ChatImageAttachment, "previewUrl"> {  previewUrl: string;  file: File;} const PersistedTerminalContextDraft = Schema.Struct({  id: Schema.String,  threadId: ThreadId,  createdAt: Schema.String,  terminalId: Schema.String,  terminalLabel: Schema.String,  lineStart: Schema.Number,  lineEnd: Schema.Number,});type PersistedTerminalContextDraft = typeof PersistedTerminalContextDraft.Type; const PersistedComposerThreadDraftState = Schema.Struct({  prompt: Schema.String,  attachments: Schema.Array(PersistedComposerImageAttachment),  terminalContexts: Schema.optionalKey(Schema.Array(PersistedTerminalContextDraft)),  modelSelectionByProvider: Schema.optionalKey(    Schema.Record(ProviderKind, Schema.optionalKey(ModelSelection)),  ),  activeProvider: Schema.optionalKey(Schema.NullOr(ProviderKind)),  runtimeMode: Schema.optionalKey(RuntimeMode),  interactionMode: Schema.optionalKey(ProviderInteractionMode),});type PersistedComposerThreadDraftState = typeof PersistedComposerThreadDraftState.Type; const LegacyCodexFields = Schema.Struct({  effort: Schema.optionalKey(Schema.Literals(CODEX_REASONING_EFFORT_OPTIONS)),  codexFastMode: Schema.optionalKey(Schema.Boolean),  serviceTier: Schema.optionalKey(Schema.String),});type LegacyCodexFields = typeof LegacyCodexFields.Type; const LegacyThreadModelFields = Schema.Struct({  provider: Schema.optionalKey(ProviderKind),  model: Schema.optionalKey(Schema.String),  modelOptions: Schema.optionalKey(Schema.NullOr(ProviderModelOptions)),});type LegacyThreadModelFields = typeof LegacyThreadModelFields.Type; type LegacyV2ThreadDraftFields = {  modelSelection?: ModelSelection | null;  modelOptions?: ProviderModelOptions | null;}; type LegacyPersistedComposerThreadDraftState = PersistedComposerThreadDraftState &  LegacyCodexFields &  LegacyThreadModelFields &  LegacyV2ThreadDraftFields; const LegacyStickyModelFields = Schema.Struct({  stickyProvider: Schema.optionalKey(ProviderKind),  stickyModel: Schema.optionalKey(Schema.String),  stickyModelOptions: Schema.optionalKey(Schema.NullOr(ProviderModelOptions)),});type LegacyStickyModelFields = typeof LegacyStickyModelFields.Type; type LegacyV2StoreFields = {  stickyModelSelection?: ModelSelection | null;  stickyModelOptions?: ProviderModelOptions | null;}; type LegacyPersistedComposerDraftStoreState = PersistedComposerDraftStoreState &  LegacyStickyModelFields &  LegacyV2StoreFields; const PersistedDraftThreadState = Schema.Struct({  projectId: ProjectId,  createdAt: Schema.String,  runtimeMode: RuntimeMode,  interactionMode: ProviderInteractionMode,  branch: Schema.NullOr(Schema.String),  worktreePath: Schema.NullOr(Schema.String),  envMode: DraftThreadEnvModeSchema,});type PersistedDraftThreadState = typeof PersistedDraftThreadState.Type; const PersistedComposerDraftStoreState = Schema.Struct({  draftsByThreadId: Schema.Record(ThreadId, PersistedComposerThreadDraftState),  draftThreadsByThreadId: Schema.Record(ThreadId, PersistedDraftThreadState),  projectDraftThreadIdByProjectId: Schema.Record(ProjectId, ThreadId),  stickyModelSelectionByProvider: Schema.optionalKey(    Schema.Record(ProviderKind, Schema.optionalKey(ModelSelection)),  ),  stickyActiveProvider: Schema.optionalKey(Schema.NullOr(ProviderKind)),});type PersistedComposerDraftStoreState = typeof PersistedComposerDraftStoreState.Type; const PersistedComposerDraftStoreStorage = Schema.Struct({  version: Schema.Number,  state: PersistedComposerDraftStoreState,}); export interface ComposerThreadDraftState {  prompt: string;  images: ComposerImageAttachment[];  nonPersistedImageIds: string[];  persistedAttachments: PersistedComposerImageAttachment[];  terminalContexts: TerminalContextDraft[];  modelSelectionByProvider: Partial<Record<ProviderKind, ModelSelection>>;  activeProvider: ProviderKind | null;  runtimeMode: RuntimeMode | null;  interactionMode: ProviderInteractionMode | null;} 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>;  stickyModelSelectionByProvider: Partial<Record<ProviderKind, ModelSelection>>;  stickyActiveProvider: ProviderKind | null;  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;  setStickyModelSelection: (modelSelection: ModelSelection | null | undefined) => void;  setPrompt: (threadId: ThreadId, prompt: string) => void;  setTerminalContexts: (threadId: ThreadId, contexts: TerminalContextDraft[]) => void;  setModelSelection: (    threadId: ThreadId,    modelSelection: ModelSelection | null | undefined,  ) => void;  setModelOptions: (    threadId: ThreadId,    modelOptions: ProviderModelOptions | null | undefined,  ) => void;  applyStickyState: (threadId: ThreadId) => void;  setProviderModelOptions: (    threadId: ThreadId,    provider: ProviderKind,    nextProviderOptions: ProviderModelOptions[ProviderKind] | null | undefined,    options?: {      persistSticky?: boolean;    },  ) => void;  setRuntimeMode: (threadId: ThreadId, runtimeMode: RuntimeMode | null | undefined) => void;  setInteractionMode: (    threadId: ThreadId,    interactionMode: ProviderInteractionMode | 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;} export interface EffectiveComposerModelState {  selectedModel: string;  modelOptions: ProviderModelOptions | null;} function providerModelOptionsFromSelection(  modelSelection: ModelSelection | null | undefined,): ProviderModelOptions | null {  if (!modelSelection?.options) {    return null;  }   return {    [modelSelection.provider]: modelSelection.options,  };} function modelSelectionByProviderToOptions(  map: Partial<Record<ProviderKind, ModelSelection>> | null | undefined,): ProviderModelOptions | null {  if (!map) return null;  const result: Record<string, unknown> = {};  for (const [provider, selection] of Object.entries(map)) {    if (selection?.options) {      result[provider] = selection.options;    }  }  return Object.keys(result).length > 0 ? (result as ProviderModelOptions) : null;} const EMPTY_PERSISTED_DRAFT_STORE_STATE = Object.freeze<PersistedComposerDraftStoreState>({  draftsByThreadId: {},  draftThreadsByThreadId: {},  projectDraftThreadIdByProjectId: {},  stickyModelSelectionByProvider: {},  stickyActiveProvider: null,}); 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);const EMPTY_MODEL_SELECTION_BY_PROVIDER: Partial<Record<ProviderKind, ModelSelection>> =  Object.freeze({}); const EMPTY_THREAD_DRAFT = Object.freeze<ComposerThreadDraftState>({  prompt: "",  images: EMPTY_IMAGES,  nonPersistedImageIds: EMPTY_IDS,  persistedAttachments: EMPTY_PERSISTED_ATTACHMENTS,  terminalContexts: EMPTY_TERMINAL_CONTEXTS,  modelSelectionByProvider: EMPTY_MODEL_SELECTION_BY_PROVIDER,  activeProvider: null,  runtimeMode: null,  interactionMode: null,}); function createEmptyThreadDraft(): ComposerThreadDraftState {  return {    prompt: "",    images: [],    nonPersistedImageIds: [],    persistedAttachments: [],    terminalContexts: [],    modelSelectionByProvider: {},    activeProvider: null,    runtimeMode: null,    interactionMode: null,  };} 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 &&    Object.keys(draft.modelSelectionByProvider).length === 0 &&    draft.activeProvider === null &&    draft.runtimeMode === null &&    draft.interactionMode === null  );} function normalizeProviderKind(value: unknown): ProviderKind | null {  return value === "codex" || value === "claudeAgent" ? value : null;} function normalizeProviderModelOptions(  value: unknown,  provider?: ProviderKind | null,  legacy?: LegacyCodexFields,): ProviderModelOptions | null {  const candidate = value && typeof value === "object" ? (value as Record<string, unknown>) : null;  const codexCandidate =    candidate?.codex && typeof candidate.codex === "object"      ? (candidate.codex as Record<string, unknown>)      : null;  const claudeCandidate =    candidate?.claudeAgent && typeof candidate.claudeAgent === "object"      ? (candidate.claudeAgent as Record<string, unknown>)      : null;   const codexReasoningEffort: CodexReasoningEffort | undefined =    codexCandidate?.reasoningEffort === "low" ||    codexCandidate?.reasoningEffort === "medium" ||    codexCandidate?.reasoningEffort === "high" ||    codexCandidate?.reasoningEffort === "xhigh"      ? codexCandidate.reasoningEffort      : provider === "codex" &&          (legacy?.effort === "low" ||            legacy?.effort === "medium" ||            legacy?.effort === "high" ||            legacy?.effort === "xhigh")        ? legacy.effort        : undefined;  const codexFastMode =    codexCandidate?.fastMode === true      ? true      : codexCandidate?.fastMode === false        ? false        : (provider === "codex" && legacy?.codexFastMode === true) ||            (typeof legacy?.serviceTier === "string" && legacy.serviceTier === "fast")          ? true          : undefined;  const codex =    codexReasoningEffort !== undefined || codexFastMode !== undefined      ? {          ...(codexReasoningEffort !== undefined ? { reasoningEffort: codexReasoningEffort } : {}),          ...(codexFastMode !== undefined ? { fastMode: codexFastMode } : {}),        }      : undefined;   const claudeThinking =    claudeCandidate?.thinking === true      ? true      : claudeCandidate?.thinking === false        ? false        : undefined;  const claudeEffort: ClaudeCodeEffort | undefined =    claudeCandidate?.effort === "low" ||    claudeCandidate?.effort === "medium" ||    claudeCandidate?.effort === "high" ||    claudeCandidate?.effort === "max" ||    claudeCandidate?.effort === "ultrathink"      ? claudeCandidate.effort      : undefined;  const claudeFastMode =    claudeCandidate?.fastMode === true      ? true      : claudeCandidate?.fastMode === false        ? false        : undefined;  const claudeContextWindow =    typeof claudeCandidate?.contextWindow === "string" && claudeCandidate.contextWindow.length > 0      ? claudeCandidate.contextWindow      : undefined;  const claude =    claudeThinking !== undefined ||    claudeEffort !== undefined ||    claudeFastMode !== undefined ||    claudeContextWindow !== undefined      ? {          ...(claudeThinking !== undefined ? { thinking: claudeThinking } : {}),          ...(claudeEffort !== undefined ? { effort: claudeEffort } : {}),          ...(claudeFastMode !== undefined ? { fastMode: claudeFastMode } : {}),          ...(claudeContextWindow !== undefined ? { contextWindow: claudeContextWindow } : {}),        }      : undefined;   if (!codex && !claude) {    return null;  }  return {    ...(codex ? { codex } : {}),    ...(claude ? { claudeAgent: claude } : {}),

Lines 1–500 of 2212.