4212 lines · 163.1 KB · TypeScript
RawDownload
import {  DEFAULT_MODEL,  DEFAULT_MODEL_BY_PROVIDER,  defaultInstanceIdForDriver,  EnvironmentId,  ModelSelection,  ProjectId,  ProviderInstanceId,  ProviderInteractionMode,  ProviderDriverKind,  ProviderOptionSelection,  PROVIDER_SEND_TURN_MAX_ATTACHMENTS,  PreviewAnnotationPayloadSchema,  type PreviewAnnotationPayload,  RuntimeMode,  type ServerProvider,  type ScopedProjectRef,  type ScopedThreadRef,  ThreadId,  SnapShotSource,} from "@t3tools/contracts";import {  parseScopedProjectKey,  parseScopedThreadKey,  scopedProjectKey,  scopeProjectRef,  scopedThreadKey,  scopeThreadRef,} from "@t3tools/client-runtime/environment";import * as Schema from "effect/Schema";import * as Equal from "effect/Equal";import * as Effect from "effect/Effect";import { DeepMutable } from "effect/Types";import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";import { useMemo } from "react";import { getLocalStorageItem } from "./hooks/useLocalStorage";import { resolveAppModelSelection, resolveAppModelSelectionForInstance } from "./modelSelection";import {  DEFAULT_INTERACTION_MODE,  DEFAULT_RUNTIME_MODE,  type ChatFileAttachment,  type ChatImageAttachment,  videoMimeType,} from "./types";import {  type TerminalContextDraft,  ensureInlineTerminalContextPlaceholders,  normalizeTerminalContextText,} from "./lib/terminalContext";import {  type ElementContextDraft,  type ElementContextSelection,  elementContextDedupKey,  newElementContextId,} from "./lib/elementContext";import { create } from "zustand";import { persist, type PersistStorage, type StorageValue } from "zustand/middleware";import { useShallow } from "zustand/react/shallow";import { createDeferredStorage, createMemoryStorage } from "./lib/storage";import { getDefaultServerModel } from "./providerModels";import { UnifiedSettings } from "@t3tools/contracts/settings";import { ReviewCommentContextSchema, type ReviewCommentContext } from "./reviewCommentContext";const isRuntimeMode = Schema.is(RuntimeMode);const isProviderDriverKind = Schema.is(ProviderDriverKind);const isReviewCommentContext = Schema.is(ReviewCommentContextSchema);const isSnapShotSource = Schema.is(SnapShotSource); export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";const COMPOSER_DRAFT_STORAGE_VERSION = 9;const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]);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; // Keep the immutable state until flush. Migration writebacks already have the persisted shape.type ComposerPersistState =  | { capturedState: ComposerDraftStoreState }  | PersistedComposerDraftStoreState; const composerDebouncedStorage = createDeferredStorage<StorageValue<ComposerPersistState>>(  typeof localStorage !== "undefined" ? localStorage : createMemoryStorage(),  (value) =>    JSON.stringify({      state:        "capturedState" in value.state          ? partializeComposerDraftStoreState(value.state.capturedState)          : value.state,      version: value.version,    }),  COMPOSER_PERSIST_DEBOUNCE_MS,); const composerPersistStorage: PersistStorage<ComposerPersistState> = {  getItem: (name) => {    // The base storage is localStorage (or in-memory), which is synchronous.    const raw = composerDebouncedStorage.getItem(name);    if (typeof raw !== "string") {      return null;    }    // Parsed persisted JSON. `migrate` and `merge` normalize it from unknown,    // so the cast mirrors the one zustand's createJSONStorage performs.    return JSON.parse(raw) as StorageValue<ComposerPersistState>;  },  setItem: (name, value) => composerDebouncedStorage.setItem(name, value),  removeItem: (name) => composerDebouncedStorage.removeItem(name),}; // 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,  source: Schema.optional(SnapShotSource),  dataUrl: Schema.String,});export type PersistedComposerImageAttachment = typeof PersistedComposerImageAttachment.Type; export interface ComposerImageAttachment extends Omit<ChatImageAttachment, "previewUrl"> {  previewUrl: string;  file: File;} export interface ComposerFileAttachment extends Omit<ChatFileAttachment, "previewUrl"> {  file: File | null;  uploadedAttachmentId?: string;  uploadEnvironmentId?: EnvironmentId;} /** * A hydrated draft file whose upload never finished before the reload has * neither bytes (`file` is only ever null after hydration) nor a server-side * upload. The composer renders it as a needs-reattach row: the user must * attach the file again or remove it before sending. */export function composerFileNeedsReattach(file: ComposerFileAttachment): boolean {  return file.file === null && file.uploadedAttachmentId === undefined;} function clearStaleFileUploadMetadata(  draft: ComposerThreadDraftState,  environmentId: EnvironmentId,): ComposerThreadDraftState {  let changed = false;  const files = draft.files.map((file) => {    if (      (file.uploadedAttachmentId === undefined && file.uploadEnvironmentId === undefined) ||      file.uploadEnvironmentId === environmentId    ) {      return file;    }     changed = true;    const nextFile = { ...file };    delete nextFile.uploadedAttachmentId;    delete nextFile.uploadEnvironmentId;    return nextFile;  });   return changed ? { ...draft, files } : draft;} export const PersistedComposerFileAttachment = Schema.Struct({  id: Schema.String,  name: Schema.String,  mimeType: Schema.String,  sizeBytes: Schema.Number,  attachmentId: Schema.String,  environmentId: EnvironmentId,});export type PersistedComposerFileAttachment = typeof PersistedComposerFileAttachment.Type; /** * Draft-persisted file. Unlike a stash entry (which requires a finished * upload), a draft may hold a file whose upload never completed. Its `File` * handle cannot serialize, so it persists as a metadata-only marker (no * `attachmentId`) and hydrates as a needs-reattach row instead of vanishing. */export const PersistedComposerDraftFileAttachment = Schema.Struct({  id: Schema.String,  name: Schema.String,  mimeType: Schema.String,  sizeBytes: Schema.Number,  attachmentId: Schema.optionalKey(Schema.String),  environmentId: Schema.optionalKey(EnvironmentId),});export type PersistedComposerDraftFileAttachment = typeof PersistedComposerDraftFileAttachment.Type;const isPersistedComposerDraftFileAttachment = Schema.is(PersistedComposerDraftFileAttachment); 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 PersistedElementContextStackFrame = Schema.Struct({  functionName: Schema.NullOr(Schema.String),  fileName: Schema.NullOr(Schema.String),  lineNumber: Schema.NullOr(Schema.Number),  columnNumber: Schema.NullOr(Schema.Number),}); const PersistedElementContextDraft = Schema.Struct({  id: Schema.String,  threadId: ThreadId,  pickedAt: Schema.String,  pageUrl: Schema.String,  pageTitle: Schema.NullOr(Schema.String),  tagName: Schema.String,  selector: Schema.NullOr(Schema.String),  htmlPreview: Schema.String,  componentName: Schema.NullOr(Schema.String),  source: Schema.NullOr(PersistedElementContextStackFrame),  styles: Schema.String,});type PersistedElementContextDraft = typeof PersistedElementContextDraft.Type; const PersistedComposerThreadDraftState = Schema.Struct({  prompt: Schema.String,  attachments: Schema.Array(PersistedComposerImageAttachment),  files: Schema.optionalKey(Schema.Array(PersistedComposerDraftFileAttachment)),  terminalContexts: Schema.optionalKey(Schema.Array(PersistedTerminalContextDraft)),  elementContexts: Schema.optionalKey(Schema.Array(PersistedElementContextDraft)),  previewAnnotations: Schema.optionalKey(Schema.Array(PreviewAnnotationPayloadSchema)),  reviewComments: Schema.optionalKey(Schema.Array(ReviewCommentContextSchema)),  // Keyed by `ProviderInstanceId` (open branded slug) so custom provider  // instances (e.g. `codex_personal`) round-trip alongside the built-in  // `codex` / `claudeAgent` / ... entries. Every prior `ProviderDriverKind`  // literal satisfies the `ProviderInstanceId` slug pattern, so existing  // persisted drafts decode unchanged.  //  // The record's value schema is NOT wrapped in `Schema.optionalKey`:  // that helper is only meaningful on property signatures with a known  // key set, and `Schema.Record(<branded string>, …)` produces an index  // signature at runtime (Schema rejects the combination). Absence of  // an entry already encodes "no selection for this instance".  modelSelectionByProvider: Schema.optionalKey(Schema.Record(ProviderInstanceId, ModelSelection)),  activeProvider: Schema.optionalKey(Schema.NullOr(ProviderInstanceId)),  // True only when a human picked this selection in the composer. Seeded  // selections (project default / sticky) leave it unset so later seeds can  // replace them; legacy entries predate the flag and read as seeded too.  modelSelectionExplicit: Schema.optionalKey(Schema.Boolean),  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<string, 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),  environmentSelection: Schema.optionalKey(Schema.Literals(["auto", "manual"])),  loadBalancedEnvironmentId: Schema.optionalKey(Schema.NullOr(Schema.String)),  createdAt: Schema.String,  runtimeMode: RuntimeMode,  interactionMode: ProviderInteractionMode,  branch: Schema.NullOr(Schema.String),  worktreePath: Schema.NullOr(Schema.String),  envMode: DraftThreadEnvModeSchema,  startFromOrigin: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),  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(ProviderInstanceId, ModelSelection),  ),  stickyActiveProvider: Schema.optionalKey(Schema.NullOr(ProviderInstanceId)),});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[];  files: ComposerFileAttachment[];  nonPersistedImageIds: string[];  persistedAttachments: PersistedComposerImageAttachment[];  terminalContexts: TerminalContextDraft[];  /**   * Element-pick attachments captured from the in-app preview browser. The   * full payload (selector / html / styles / source frame) is persisted   * inline because — unlike terminal contexts — there's no live session to   * re-derive the snapshot from on reload.   */  elementContexts: ElementContextDraft[];  previewAnnotations: PreviewAnnotationPayload[];  reviewComments: ReviewCommentContext[];  /**   * Per-instance model selection. Keyed by `ProviderInstanceId` (open   * branded slug) so a default `codex` instance and a user-authored   * `codex_personal` instance each persist their own selected model. Every   * historical `ProviderDriverKind` literal (`codex` / `claudeAgent` / `cursor` /   * `opencode`) also satisfies the `ProviderInstanceId` slug pattern, so   * legacy kind-keyed drafts round-trip unchanged.   */  modelSelectionByProvider: Partial<Record<ProviderInstanceId, ModelSelection>>;  /** Routing key of the last picked instance (see `modelSelectionByProvider`). */  activeProvider: ProviderInstanceId | null;  /**   * True only when a human picked the active selection in the composer.   * Absent/false means seeded (project default / sticky), so later seeds   * may replace it. Legacy entries predate the flag and read as seeded.   */  modelSelectionExplicit?: boolean;  runtimeMode: RuntimeMode | null;  interactionMode: ProviderInteractionMode | null;} /** * True when the user has invested real content in the draft: typed text or * any attachment/context. Model selection and mode choices alone do not * count — those are ambient defaults, not work in progress. Used by the * sidebar draft rows (which draft sessions deserve a row) and by new-thread * resurrection (a draft with content keeps its settings instead of being * reset to defaults). */export function composerDraftHasUserContent(  draft: ComposerThreadDraftState | null | undefined,): boolean {  if (!draft) {    return false;  }  return (    draft.prompt.trim().length > 0 ||    draft.images.length > 0 ||    draft.files.length > 0 ||    draft.persistedAttachments.length > 0 ||    draft.terminalContexts.length > 0 ||    draft.elementContexts.length > 0 ||    draft.previewAnnotations.length > 0 ||    draft.reviewComments.length > 0  );} /** * 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;  environmentSelection?: "auto" | "manual";  loadBalancedEnvironmentId?: EnvironmentId | null;  createdAt: string;  runtimeMode: RuntimeMode;  interactionMode: ProviderInteractionMode;  branch: string | null;  worktreePath: string | null;  envMode: DraftThreadEnvMode;  startFromOrigin: boolean;  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. */export 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>;  backgroundSubmissionThreadKeys: Record<string, true>;  stickyModelSelectionByProvider: Partial<Record<ProviderInstanceId, ModelSelection>>;  stickyActiveProvider: ProviderInstanceId | 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.   * Reassigning an existing draft removes its previous logical-project   * mapping so one session cannot resolve from two projects.   */  setLogicalProjectDraftThreadId: (

Lines 1–500 of 4212.