import { DEFAULT_MODEL, DEFAULT_MODEL_BY_PROVIDER, defaultInstanceIdForDriver, type EnvironmentId, ModelSelection, ProjectId, ProviderInstanceId, ProviderInteractionMode, ProviderDriverKind, ProviderOptionSelection, PreviewAnnotationPayloadSchema, type PreviewAnnotationPayload, RuntimeMode, type ServerProvider, type ScopedProjectRef, type ScopedThreadRef, ThreadId,} 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 ChatImageAttachment } 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 { 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";import { ReviewCommentContextSchema, type ReviewCommentContext } from "./reviewCommentContext";const isRuntimeMode = Schema.is(RuntimeMode);const isProviderDriverKind = Schema.is(ProviderDriverKind);const isReviewCommentContext = Schema.is(ReviewCommentContextSchema); export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";const COMPOSER_DRAFT_STORAGE_VERSION = 8;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; 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 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), 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)), 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), 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[]; 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; 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; 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. */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<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. */ setLogicalProjectDraftThreadId: ( logicalProjectKey: string, projectRef: ScopedProjectRef, draftId: DraftId, options?: { threadId?: ThreadId; branch?: string | null; worktreePath?: string | null; createdAt?: string; envMode?: DraftThreadEnvMode; startFromOrigin?: boolean; 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; startFromOrigin?: boolean; 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; startFromOrigin?: boolean; 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, opts?: { /** * Replace the stored entry outright instead of preserving its * existing options when the incoming selection has none. Used when * the selection is a complete snapshot (e.g. carried from another * thread) rather than a model-only change. */ replaceOptions?: boolean; }, ) => void; /** Replace the model options for one or more providers in the draft. */ setModelOptions: ( threadRef: ComposerThreadTarget, modelOptions: | Partial<Record<string, ReadonlyArray<ProviderOptionSelection>>> | null | undefined, ) => void; applyStickyState: (threadRef: ComposerThreadTarget) => void; setProviderModelOptions: ( threadRef: ComposerThreadTarget, provider: ProviderDriverKind, nextProviderOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined, options?: { instanceId?: ProviderInstanceId | null | undefined; 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; /** * Append a fresh element pick to the draft. Returns true when accepted, * false when deduped against an existing pick of the same element. */ addElementContext: ( threadRef: ComposerThreadTarget, selection: ElementContextSelection, ) => boolean; /** * Replace the entire element-contexts list (used by send-failure retry to * restore the pre-send snapshot). */ setElementContexts: ( threadRef: ComposerThreadTarget, contexts: ReadonlyArray<ElementContextDraft>, ) => void; removeElementContext: (threadRef: ComposerThreadTarget, contextId: string) => void; clearElementContexts: (threadRef: ComposerThreadTarget) => void; addPreviewAnnotation: ( threadRef: ComposerThreadTarget, annotation: PreviewAnnotationPayload, ) => void; setPreviewAnnotations: ( threadRef: ComposerThreadTarget, annotations: ReadonlyArray<PreviewAnnotationPayload>, ) => void; removePreviewAnnotation: (threadRef: ComposerThreadTarget, annotationId: string) => void; addReviewComment: (threadRef: ComposerThreadTarget, comment: ReviewCommentContext) => void; setReviewComments: ( threadRef: ComposerThreadTarget, comments: ReadonlyArray<ReviewCommentContext>, ) => void; removeReviewComment: (threadRef: ComposerThreadTarget, commentId: string) => void; clearPersistedAttachments: (threadRef: ComposerThreadTarget) => void; syncPersistedAttachments: ( threadRef: ComposerThreadTarget, attachments: PersistedComposerImageAttachment[], ) => void; clearComposerContent: (threadRef: ComposerThreadTarget) => void;} export interface EffectiveComposerModelState {Lines 1–500 of 3574. Load more
import { DEFAULT_MODEL, DEFAULT_MODEL_BY_PROVIDER, defaultInstanceIdForDriver, type EnvironmentId, ModelSelection, ProjectId, ProviderInstanceId, ProviderInteractionMode, ProviderDriverKind, ProviderOptionSelection, PreviewAnnotationPayloadSchema, type PreviewAnnotationPayload, RuntimeMode, type ServerProvider, type ScopedProjectRef, type ScopedThreadRef, ThreadId, } 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 ChatImageAttachment } 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 { 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"; import { ReviewCommentContextSchema, type ReviewCommentContext } from "./reviewCommentContext"; const isRuntimeMode = Schema.is(RuntimeMode); const isProviderDriverKind = Schema.is(ProviderDriverKind); const isReviewCommentContext = Schema.is(ReviewCommentContextSchema); export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"; const COMPOSER_DRAFT_STORAGE_VERSION = 8; 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; 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 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), 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)), 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), 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[]; 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; 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; 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. */ 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<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. */ setLogicalProjectDraftThreadId: ( logicalProjectKey: string, projectRef: ScopedProjectRef, draftId: DraftId, options?: { threadId?: ThreadId; branch?: string | null; worktreePath?: string | null; createdAt?: string; envMode?: DraftThreadEnvMode; startFromOrigin?: boolean; 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; startFromOrigin?: boolean; 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; startFromOrigin?: boolean; 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, opts?: { /** * Replace the stored entry outright instead of preserving its * existing options when the incoming selection has none. Used when * the selection is a complete snapshot (e.g. carried from another * thread) rather than a model-only change. */ replaceOptions?: boolean; }, ) => void; /** Replace the model options for one or more providers in the draft. */ setModelOptions: ( threadRef: ComposerThreadTarget, modelOptions: | Partial<Record<string, ReadonlyArray<ProviderOptionSelection>>> | null | undefined, ) => void; applyStickyState: (threadRef: ComposerThreadTarget) => void; setProviderModelOptions: ( threadRef: ComposerThreadTarget, provider: ProviderDriverKind, nextProviderOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined, options?: { instanceId?: ProviderInstanceId | null | undefined; 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; /** * Append a fresh element pick to the draft. Returns true when accepted, * false when deduped against an existing pick of the same element. */ addElementContext: ( threadRef: ComposerThreadTarget, selection: ElementContextSelection, ) => boolean; /** * Replace the entire element-contexts list (used by send-failure retry to * restore the pre-send snapshot). */ setElementContexts: ( threadRef: ComposerThreadTarget, contexts: ReadonlyArray<ElementContextDraft>, ) => void; removeElementContext: (threadRef: ComposerThreadTarget, contextId: string) => void; clearElementContexts: (threadRef: ComposerThreadTarget) => void; addPreviewAnnotation: ( threadRef: ComposerThreadTarget, annotation: PreviewAnnotationPayload, ) => void; setPreviewAnnotations: ( threadRef: ComposerThreadTarget, annotations: ReadonlyArray<PreviewAnnotationPayload>, ) => void; removePreviewAnnotation: (threadRef: ComposerThreadTarget, annotationId: string) => void; addReviewComment: (threadRef: ComposerThreadTarget, comment: ReviewCommentContext) => void; setReviewComments: ( threadRef: ComposerThreadTarget, comments: ReadonlyArray<ReviewCommentContext>, ) => void; removeReviewComment: (threadRef: ComposerThreadTarget, commentId: string) => void; clearPersistedAttachments: (threadRef: ComposerThreadTarget) => void; syncPersistedAttachments: ( threadRef: ComposerThreadTarget, attachments: PersistedComposerImageAttachment[], ) => void; clearComposerContent: (threadRef: ComposerThreadTarget) => void; } export interface EffectiveComposerModelState {