2893 lines · 106.4 KB · TypeScript
RawDownload
import {  CODEX_REASONING_EFFORT_OPTIONS,  type ClaudeCodeEffort,  type CodexReasoningEffort,  DEFAULT_MODEL_BY_PROVIDER,  type EnvironmentId,  ModelSelection,  ProjectId,  ProviderInteractionMode,  ProviderKind,  ProviderModelOptions,  RuntimeMode,  type ServerProvider,  type ScopedProjectRef,  type ScopedThreadRef,  ThreadId,} from "@t3tools/contracts";import {  parseScopedProjectKey,  parseScopedThreadKey,  scopedProjectKey,  scopeProjectRef,  scopedThreadKey,  scopeThreadRef,} from "@t3tools/client-runtime";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 { useShallow } from "zustand/react/shallow";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 = 5;const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]);const isRuntimeMode = Schema.is(RuntimeMode);export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type; export const DraftId = Schema.String.pipe(Schema.brand("DraftId"));export type DraftId = typeof DraftId.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" && typeof window.addEventListener === "function") {  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;  projectDraftThreadIdByProjectId?: Record<string, string> | null;  draftsByThreadId?: Record<string, PersistedComposerThreadDraftState> | null;  draftThreadsByThreadId?: Record<string, PersistedDraftThreadState> | null;  projectDraftThreadIdByProjectKey?: Record<string, string> | null;  draftsByThreadKey?: Record<string, PersistedComposerThreadDraftState> | null;  draftThreadsByThreadKey?: Record<string, PersistedDraftThreadState> | null;  projectDraftThreadKeyByProjectKey?: Record<string, string> | null;  logicalProjectDraftThreadKeyByLogicalProjectKey?: Record<string, string> | null;}; type LegacyPersistedComposerDraftStoreState = PersistedComposerDraftStoreState &  LegacyStickyModelFields &  LegacyV2StoreFields; const PersistedDraftThreadState = Schema.Struct({  threadId: ThreadId,  environmentId: Schema.String,  projectId: ProjectId,  logicalProjectKey: Schema.optionalKey(Schema.String),  createdAt: Schema.String,  runtimeMode: RuntimeMode,  interactionMode: ProviderInteractionMode,  branch: Schema.NullOr(Schema.String),  worktreePath: Schema.NullOr(Schema.String),  envMode: DraftThreadEnvModeSchema,  promotedTo: Schema.optionalKey(    Schema.NullOr(      Schema.Struct({        environmentId: Schema.String,        threadId: Schema.String,      }),    ),  ),});type PersistedDraftThreadState = typeof PersistedDraftThreadState.Type; const PersistedComposerDraftStoreState = Schema.Struct({  draftsByThreadKey: Schema.Record(Schema.String, PersistedComposerThreadDraftState),  draftThreadsByThreadKey: Schema.Record(Schema.String, PersistedDraftThreadState),  logicalProjectDraftThreadKeyByLogicalProjectKey: Schema.Record(Schema.String, Schema.String),  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,}); /** * Composer content keyed by either a draft session (`DraftId`) or a real server * thread (`ScopedThreadRef`). This is the editable payload shown in the composer. */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;} /** * Mutable routing and execution context for a pre-thread draft session. * * Unlike a real server thread, a draft session can still change target * environment/worktree configuration before the first send. */export interface DraftSessionState {  threadId: ThreadId;  environmentId: EnvironmentId;  projectId: ProjectId;  logicalProjectKey: string;  createdAt: string;  runtimeMode: RuntimeMode;  interactionMode: ProviderInteractionMode;  branch: string | null;  worktreePath: string | null;  envMode: DraftThreadEnvMode;  promotedTo?: ScopedThreadRef | null;} export type DraftThreadState = DraftSessionState; /** * Draft session metadata paired with its stable draft-session identity. */interface ProjectDraftSession extends DraftSessionState {  draftId: DraftId;} /** * App-facing composer identity: * - `DraftId` for pre-thread draft sessions * - `ScopedThreadRef` for server-backed threads * * Raw `ThreadId` is intentionally excluded so callers cannot drop environment * identity for real threads. */type ComposerThreadTarget = ScopedThreadRef | DraftId; /** * Persisted store for composer content plus draft-session metadata. * * The store intentionally models two domains: * - draft sessions keyed by `DraftId` * - server thread composer state keyed by `ScopedThreadRef` */interface ComposerDraftStoreState {  draftsByThreadKey: Record<string, ComposerThreadDraftState>;  draftThreadsByThreadKey: Record<string, DraftThreadState>;  logicalProjectDraftThreadKeyByLogicalProjectKey: Record<string, string>;  stickyModelSelectionByProvider: Partial<Record<ProviderKind, ModelSelection>>;  stickyActiveProvider: ProviderKind | null;  /** Returns the editable composer content for a draft session or server thread. */  getComposerDraft: (target: ComposerThreadTarget) => ComposerThreadDraftState | null;  /** Looks up the active draft session for a logical project identity. */  getDraftThreadByLogicalProjectKey: (logicalProjectKey: string) => ProjectDraftSession | null;  getDraftSessionByLogicalProjectKey: (logicalProjectKey: string) => ProjectDraftSession | null;  getDraftThreadByProjectRef: (projectRef: ScopedProjectRef) => ProjectDraftSession | null;  getDraftSessionByProjectRef: (projectRef: ScopedProjectRef) => ProjectDraftSession | null;  /** Reads mutable draft-session metadata by `DraftId`. */  getDraftSession: (draftId: DraftId) => DraftSessionState | null;  /** Resolves a server-thread ref back to a matching draft session when one exists. */  getDraftSessionByRef: (threadRef: ScopedThreadRef) => DraftSessionState | null;  getDraftThreadByRef: (threadRef: ScopedThreadRef) => DraftThreadState | null;  getDraftThread: (threadRef: ComposerThreadTarget) => DraftThreadState | null;  listDraftThreadKeys: () => string[];  hasDraftThreadsInEnvironment: (environmentId: EnvironmentId) => boolean;  /** Creates or updates the draft session tracked for a logical project. */  setLogicalProjectDraftThreadId: (    logicalProjectKey: string,    projectRef: ScopedProjectRef,    draftId: DraftId,    options?: {      threadId?: ThreadId;      branch?: string | null;      worktreePath?: string | null;      createdAt?: string;      envMode?: DraftThreadEnvMode;      runtimeMode?: RuntimeMode;      interactionMode?: ProviderInteractionMode;    },  ) => void;  /** Creates or updates the draft session tracked for a concrete project ref. */  setProjectDraftThreadId: (    projectRef: ScopedProjectRef,    draftId: DraftId,    options?: {      threadId?: ThreadId;      branch?: string | null;      worktreePath?: string | null;      createdAt?: string;      envMode?: DraftThreadEnvMode;      runtimeMode?: RuntimeMode;      interactionMode?: ProviderInteractionMode;    },  ) => void;  /** Updates mutable draft-session metadata without touching composer content. */  setDraftThreadContext: (    threadRef: ComposerThreadTarget,    options: {      branch?: string | null;      worktreePath?: string | null;      projectRef?: ScopedProjectRef;      createdAt?: string;      envMode?: DraftThreadEnvMode;      runtimeMode?: RuntimeMode;      interactionMode?: ProviderInteractionMode;    },  ) => void;  clearProjectDraftThreadId: (projectRef: ScopedProjectRef) => void;  clearProjectDraftThreadById: (    projectRef: ScopedProjectRef,    threadRef: ComposerThreadTarget,  ) => void;  /** Marks a draft session as being promoted to a real server thread. */  markDraftThreadPromoting: (threadRef: ComposerThreadTarget, promotedTo?: ScopedThreadRef) => void;  /** Removes draft-session metadata after promotion is complete. */  finalizePromotedDraftThread: (threadRef: ComposerThreadTarget) => void;  clearDraftThread: (threadRef: ComposerThreadTarget) => void;  setStickyModelSelection: (modelSelection: ModelSelection | null | undefined) => void;  setPrompt: (threadRef: ComposerThreadTarget, prompt: string) => void;  setTerminalContexts: (threadRef: ComposerThreadTarget, contexts: TerminalContextDraft[]) => void;  setModelSelection: (    threadRef: ComposerThreadTarget,    modelSelection: ModelSelection | null | undefined,  ) => void;  setModelOptions: (    threadRef: ComposerThreadTarget,    modelOptions: ProviderModelOptions | null | undefined,  ) => void;  applyStickyState: (threadRef: ComposerThreadTarget) => void;  setProviderModelOptions: (    threadRef: ComposerThreadTarget,    provider: ProviderKind,    nextProviderOptions: ProviderModelOptions[ProviderKind] | null | undefined,    options?: {      persistSticky?: boolean;    },  ) => void;  setRuntimeMode: (    threadRef: ComposerThreadTarget,    runtimeMode: RuntimeMode | null | undefined,  ) => void;  setInteractionMode: (    threadRef: ComposerThreadTarget,    interactionMode: ProviderInteractionMode | null | undefined,  ) => void;  addImage: (threadRef: ComposerThreadTarget, image: ComposerImageAttachment) => void;  addImages: (threadRef: ComposerThreadTarget, images: ComposerImageAttachment[]) => void;  removeImage: (threadRef: ComposerThreadTarget, imageId: string) => void;  insertTerminalContext: (    threadRef: ComposerThreadTarget,    prompt: string,    context: TerminalContextDraft,    index: number,  ) => boolean;  addTerminalContext: (threadRef: ComposerThreadTarget, context: TerminalContextDraft) => void;  addTerminalContexts: (threadRef: ComposerThreadTarget, contexts: TerminalContextDraft[]) => void;  removeTerminalContext: (threadRef: ComposerThreadTarget, contextId: string) => void;  clearTerminalContexts: (threadRef: ComposerThreadTarget) => void;  clearPersistedAttachments: (threadRef: ComposerThreadTarget) => void;  syncPersistedAttachments: (    threadRef: ComposerThreadTarget,    attachments: PersistedComposerImageAttachment[],  ) => void;  clearComposerContent: (threadRef: ComposerThreadTarget) => void;} export interface EffectiveComposerModelState {  selectedModel: string;  modelOptions: ProviderModelOptions | null;} interface ComposerDraftModelState {  activeProvider: ProviderKind | null;  modelSelectionByProvider: Partial<Record<ProviderKind, ModelSelection>>;} 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>({  draftsByThreadKey: {},  draftThreadsByThreadKey: {},  logicalProjectDraftThreadKeyByLogicalProjectKey: {},  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_COMPOSER_DRAFT_MODEL_STATE = Object.freeze<ComposerDraftModelState>({  activeProvider: null,  modelSelectionByProvider: EMPTY_MODEL_SELECTION_BY_PROVIDER,}); 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) {

Lines 1–500 of 2893.