2912 lines · 107.4 KB · TypeScript
RawDownload
import {  DEFAULT_MODEL_BY_PROVIDER,  type EnvironmentId,  ModelSelection,  ProjectId,  ProviderInteractionMode,  ProviderKind,  ProviderOptionSelection,  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 { createModelSelection, 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 = 6;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; /** * Per-provider record of generic option selections. Used as a transient * representation when migrating legacy v2 storage payloads and when * deriving per-provider option bundles for downstream consumers. */type ProviderOptionSelectionsByProvider = Partial<  Record<ProviderKind, ReadonlyArray<ProviderOptionSelection>>>; type LegacyCodexFields = {  effort?: unknown;  codexFastMode?: unknown;  serviceTier?: unknown;}; type LegacyThreadModelFields = {  provider?: unknown;  model?: unknown;  modelOptions?: unknown;}; type LegacyV2ThreadDraftFields = {  modelSelection?: ModelSelection | null;  modelOptions?: unknown;}; type LegacyPersistedComposerThreadDraftState = PersistedComposerThreadDraftState &  LegacyCodexFields &  LegacyThreadModelFields &  LegacyV2ThreadDraftFields; type LegacyStickyModelFields = {  stickyProvider?: unknown;  stickyModel?: unknown;  stickyModelOptions?: unknown;}; type LegacyV2StoreFields = {  stickyModelSelection?: ModelSelection | null;  stickyModelOptions?: unknown;  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;  /** Replace the model options for one or more providers in the draft. */  setModelOptions: (    threadRef: ComposerThreadTarget,    modelOptions:      | Partial<Record<ProviderKind, ReadonlyArray<ProviderOptionSelection>>>      | null      | undefined,  ) => void;  applyStickyState: (threadRef: ComposerThreadTarget) => void;  setProviderModelOptions: (    threadRef: ComposerThreadTarget,    provider: ProviderKind,    nextProviderOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined,    options?: {      model?: string | null | undefined;      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: ProviderOptionSelectionsByProvider | null;} interface ComposerDraftModelState {  activeProvider: ProviderKind | null;  modelSelectionByProvider: Partial<Record<ProviderKind, ModelSelection>>;} function providerSelectionsFromModelSelection(  modelSelection: ModelSelection | null | undefined,): ProviderOptionSelectionsByProvider | null {  if (!modelSelection) {    return null;  }  const options = modelSelection.options;  if (!options || options.length === 0) {    return null;  }  return { [modelSelection.provider]: options } as ProviderOptionSelectionsByProvider;} function modelSelectionByProviderToOptions(  map: Partial<Record<ProviderKind, ModelSelection>> | null | undefined,): ProviderOptionSelectionsByProvider | null {  if (!map) return null;  const result: Partial<Record<ProviderKind, ReadonlyArray<ProviderOptionSelection>>> = {};  for (const [provider, selection] of Object.entries(map)) {    if (selection?.options && selection.options.length > 0) {      result[provider as ProviderKind] = selection.options;    }  }  return Object.keys(result).length > 0 ? result : null;} function cloneModelSelection(selection: ModelSelection): DeepMutable<ModelSelection> {  return {    ...selection,    ...(selection.options ? { options: selection.options.map((option) => ({ ...option })) } : {}),  } as DeepMutable<ModelSelection>;} function cloneModelSelectionByProvider(  selections: Partial<Record<ProviderKind, ModelSelection>>,): DeepMutable<Partial<Record<ProviderKind, ModelSelection>>> {  return Object.fromEntries(    Object.entries(selections).map(([provider, selection]) => [      provider,      selection ? cloneModelSelection(selection) : selection,    ]),  ) as DeepMutable<Partial<Record<ProviderKind, ModelSelection>>>;} 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 {

Lines 1–500 of 2912.