import { CODEX_REASONING_EFFORT_OPTIONS, type ClaudeCodeEffort, type CodexReasoningEffort, DEFAULT_REASONING_EFFORT_BY_PROVIDER, ProjectId, ProviderInteractionMode, ProviderKind, ProviderModelOptions, RuntimeMode, ThreadId,} from "@t3tools/contracts";import * as Schema from "effect/Schema";import { DeepMutable } from "effect/Types";import { normalizeModelSlug } from "@t3tools/shared/model";import { getLocalStorageItem } from "./hooks/useLocalStorage";import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type ChatImageAttachment } from "./types";import { type TerminalContextDraft, ensureInlineTerminalContextPlaceholders, normalizeTerminalContextText,} from "./lib/terminalContext";import { create } from "zustand";import { createJSONStorage, persist } from "zustand/middleware";import { createDebouncedStorage, createMemoryStorage } from "./lib/storage"; export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";const COMPOSER_DRAFT_STORAGE_VERSION = 2;const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]);export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type; const COMPOSER_PERSIST_DEBOUNCE_MS = 300; const composerDebouncedStorage = createDebouncedStorage( typeof localStorage !== "undefined" ? localStorage : createMemoryStorage(), COMPOSER_PERSIST_DEBOUNCE_MS,); // Flush pending composer draft writes before page unload to prevent data loss.if (typeof window !== "undefined") { window.addEventListener("beforeunload", () => { composerDebouncedStorage.flush(); });} export const PersistedComposerImageAttachment = Schema.Struct({ id: Schema.String, name: Schema.String, mimeType: Schema.String, sizeBytes: Schema.Number, dataUrl: Schema.String,});export type PersistedComposerImageAttachment = typeof PersistedComposerImageAttachment.Type; export interface ComposerImageAttachment extends Omit<ChatImageAttachment, "previewUrl"> { previewUrl: string; file: File;} const PersistedTerminalContextDraft = Schema.Struct({ id: Schema.String, threadId: ThreadId, createdAt: Schema.String, terminalId: Schema.String, terminalLabel: Schema.String, lineStart: Schema.Number, lineEnd: Schema.Number,});type PersistedTerminalContextDraft = typeof PersistedTerminalContextDraft.Type; const PersistedComposerThreadDraftState = Schema.Struct({ prompt: Schema.String, attachments: Schema.Array(PersistedComposerImageAttachment), terminalContexts: Schema.optionalKey(Schema.Array(PersistedTerminalContextDraft)), provider: Schema.optionalKey(ProviderKind), model: Schema.optionalKey(Schema.String), modelOptions: Schema.optionalKey(ProviderModelOptions), 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; type LegacyPersistedCodexThreadDraftState = PersistedComposerThreadDraftState & LegacyCodexFields; const PersistedDraftThreadState = Schema.Struct({ projectId: ProjectId, createdAt: Schema.String, runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode, branch: Schema.NullOr(Schema.String), worktreePath: Schema.NullOr(Schema.String), envMode: DraftThreadEnvModeSchema,});type PersistedDraftThreadState = typeof PersistedDraftThreadState.Type; const PersistedComposerDraftStoreState = Schema.Struct({ draftsByThreadId: Schema.Record(ThreadId, PersistedComposerThreadDraftState), draftThreadsByThreadId: Schema.Record(ThreadId, PersistedDraftThreadState), projectDraftThreadIdByProjectId: Schema.Record(ProjectId, ThreadId),});type PersistedComposerDraftStoreState = typeof PersistedComposerDraftStoreState.Type; const PersistedComposerDraftStoreStorage = Schema.Struct({ version: Schema.Number, state: PersistedComposerDraftStoreState,}); interface ComposerThreadDraftState { prompt: string; images: ComposerImageAttachment[]; nonPersistedImageIds: string[]; persistedAttachments: PersistedComposerImageAttachment[]; terminalContexts: TerminalContextDraft[]; provider: ProviderKind | null; model: string | null; modelOptions: ProviderModelOptions | null; runtimeMode: RuntimeMode | null; interactionMode: ProviderInteractionMode | null;} export interface DraftThreadState { projectId: ProjectId; createdAt: string; runtimeMode: RuntimeMode; interactionMode: ProviderInteractionMode; branch: string | null; worktreePath: string | null; envMode: DraftThreadEnvMode;} interface ProjectDraftThread extends DraftThreadState { threadId: ThreadId;} interface ComposerDraftStoreState { draftsByThreadId: Record<ThreadId, ComposerThreadDraftState>; draftThreadsByThreadId: Record<ThreadId, DraftThreadState>; projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>; getDraftThreadByProjectId: (projectId: ProjectId) => ProjectDraftThread | null; getDraftThread: (threadId: ThreadId) => DraftThreadState | null; setProjectDraftThreadId: ( projectId: ProjectId, threadId: ThreadId, options?: { branch?: string | null; worktreePath?: string | null; createdAt?: string; envMode?: DraftThreadEnvMode; runtimeMode?: RuntimeMode; interactionMode?: ProviderInteractionMode; }, ) => void; setDraftThreadContext: ( threadId: ThreadId, options: { branch?: string | null; worktreePath?: string | null; projectId?: ProjectId; createdAt?: string; envMode?: DraftThreadEnvMode; runtimeMode?: RuntimeMode; interactionMode?: ProviderInteractionMode; }, ) => void; clearProjectDraftThreadId: (projectId: ProjectId) => void; clearProjectDraftThreadById: (projectId: ProjectId, threadId: ThreadId) => void; clearDraftThread: (threadId: ThreadId) => void; setPrompt: (threadId: ThreadId, prompt: string) => void; setTerminalContexts: (threadId: ThreadId, contexts: TerminalContextDraft[]) => void; setProvider: (threadId: ThreadId, provider: ProviderKind | null | undefined) => void; setModel: (threadId: ThreadId, model: string | null | undefined) => void; setModelOptions: ( threadId: ThreadId, modelOptions: ProviderModelOptions | null | undefined, ) => void; setRuntimeMode: (threadId: ThreadId, runtimeMode: RuntimeMode | null | undefined) => void; setInteractionMode: ( threadId: ThreadId, interactionMode: ProviderInteractionMode | null | undefined, ) => void; addImage: (threadId: ThreadId, image: ComposerImageAttachment) => void; addImages: (threadId: ThreadId, images: ComposerImageAttachment[]) => void; removeImage: (threadId: ThreadId, imageId: string) => void; insertTerminalContext: ( threadId: ThreadId, prompt: string, context: TerminalContextDraft, index: number, ) => boolean; addTerminalContext: (threadId: ThreadId, context: TerminalContextDraft) => void; addTerminalContexts: (threadId: ThreadId, contexts: TerminalContextDraft[]) => void; removeTerminalContext: (threadId: ThreadId, contextId: string) => void; clearTerminalContexts: (threadId: ThreadId) => void; clearPersistedAttachments: (threadId: ThreadId) => void; syncPersistedAttachments: ( threadId: ThreadId, attachments: PersistedComposerImageAttachment[], ) => void; clearComposerContent: (threadId: ThreadId) => void; clearThreadDraft: (threadId: ThreadId) => void;} const EMPTY_PERSISTED_DRAFT_STORE_STATE: PersistedComposerDraftStoreState = { draftsByThreadId: {}, draftThreadsByThreadId: {}, projectDraftThreadIdByProjectId: {},}; 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_THREAD_DRAFT = Object.freeze<ComposerThreadDraftState>({ prompt: "", images: EMPTY_IMAGES, nonPersistedImageIds: EMPTY_IDS, persistedAttachments: EMPTY_PERSISTED_ATTACHMENTS, terminalContexts: EMPTY_TERMINAL_CONTEXTS, provider: null, model: null, modelOptions: null, runtimeMode: null, interactionMode: null,}); function createEmptyThreadDraft(): ComposerThreadDraftState { return { prompt: "", images: [], nonPersistedImageIds: [], persistedAttachments: [], terminalContexts: [], provider: null, model: null, modelOptions: null, runtimeMode: null, interactionMode: null, };} function composerImageDedupKey(image: ComposerImageAttachment): string { // Keep this independent from File.lastModified so dedupe is stable for hydrated // images reconstructed from localStorage (which get a fresh lastModified value). return `${image.mimeType}\u0000${image.sizeBytes}\u0000${image.name}`;} function terminalContextDedupKey(context: TerminalContextDraft): string { return `${context.terminalId}\u0000${context.lineStart}\u0000${context.lineEnd}`;} function normalizeTerminalContextForThread( threadId: ThreadId, context: TerminalContextDraft,): TerminalContextDraft | null { const terminalId = context.terminalId.trim(); const terminalLabel = context.terminalLabel.trim(); if (terminalId.length === 0 || terminalLabel.length === 0) { return null; } const lineStart = Math.max(1, Math.floor(context.lineStart)); const lineEnd = Math.max(lineStart, Math.floor(context.lineEnd)); return { ...context, threadId, terminalId, terminalLabel, lineStart, lineEnd, text: normalizeTerminalContextText(context.text), };} function normalizeTerminalContextsForThread( threadId: ThreadId, contexts: ReadonlyArray<TerminalContextDraft>,): TerminalContextDraft[] { const existingIds = new Set<string>(); const existingDedupKeys = new Set<string>(); const normalizedContexts: TerminalContextDraft[] = []; for (const context of contexts) { const normalizedContext = normalizeTerminalContextForThread(threadId, context); if (!normalizedContext) { continue; } const dedupKey = terminalContextDedupKey(normalizedContext); if (existingIds.has(normalizedContext.id) || existingDedupKeys.has(dedupKey)) { continue; } normalizedContexts.push(normalizedContext); existingIds.add(normalizedContext.id); existingDedupKeys.add(dedupKey); } return normalizedContexts;} function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { return ( draft.prompt.length === 0 && draft.images.length === 0 && draft.persistedAttachments.length === 0 && draft.terminalContexts.length === 0 && draft.provider === null && draft.model === null && draft.modelOptions === null && draft.runtimeMode === null && draft.interactionMode === null );} function normalizeProviderKind(value: unknown): ProviderKind | null { return value === "codex" || value === "claudeAgent" ? value : null;} function normalizeProviderModelOptions( value: unknown, provider?: ProviderKind | null, legacy?: LegacyCodexFields,): ProviderModelOptions | null { const candidate = value && typeof value === "object" ? (value as Record<string, unknown>) : null; const codexCandidate = candidate?.codex && typeof candidate.codex === "object" ? (candidate.codex as Record<string, unknown>) : null; const claudeCandidate = candidate?.claudeAgent && typeof candidate.claudeAgent === "object" ? (candidate.claudeAgent as Record<string, unknown>) : null; const codexReasoningEffort: CodexReasoningEffort | undefined = codexCandidate?.reasoningEffort === "low" || codexCandidate?.reasoningEffort === "medium" || codexCandidate?.reasoningEffort === "high" || codexCandidate?.reasoningEffort === "xhigh" ? codexCandidate.reasoningEffort : provider === "codex" && (legacy?.effort === "low" || legacy?.effort === "medium" || legacy?.effort === "high" || legacy?.effort === "xhigh") ? legacy.effort : undefined; const codexFastMode = codexCandidate?.fastMode === true || (provider === "codex" && legacy?.codexFastMode === true) || (typeof legacy?.serviceTier === "string" && legacy.serviceTier === "fast"); const codex = codexReasoningEffort && codexReasoningEffort !== DEFAULT_REASONING_EFFORT_BY_PROVIDER.codex ? { reasoningEffort: codexReasoningEffort, ...(codexFastMode ? { fastMode: true } : {}), } : codexFastMode ? { fastMode: true } : undefined; const claudeThinking = claudeCandidate?.thinking === false ? false : undefined; const claudeEffort: ClaudeCodeEffort | undefined = claudeCandidate?.effort === "low" || claudeCandidate?.effort === "medium" || claudeCandidate?.effort === "high" || claudeCandidate?.effort === "max" || claudeCandidate?.effort === "ultrathink" ? claudeCandidate.effort : undefined; const claudeFastMode = claudeCandidate?.fastMode === true; const claude = claudeThinking === false || (claudeEffort && claudeEffort !== DEFAULT_REASONING_EFFORT_BY_PROVIDER.claudeAgent) || claudeFastMode ? { ...(claudeThinking === false ? { thinking: false } : {}), ...(claudeEffort && claudeEffort !== DEFAULT_REASONING_EFFORT_BY_PROVIDER.claudeAgent ? { effort: claudeEffort } : {}), ...(claudeFastMode ? { fastMode: true } : {}), } : undefined; if (!codex && !claude) { return null; } return { ...(codex ? { codex } : {}), ...(claude ? { claudeAgent: claude } : {}), };} function revokeObjectPreviewUrl(previewUrl: string): void { if (typeof URL === "undefined") { return; } if (!previewUrl.startsWith("blob:")) { return; } URL.revokeObjectURL(previewUrl);} function normalizePersistedAttachment(value: unknown): PersistedComposerImageAttachment | null { if (!value || typeof value !== "object") { return null; } const candidate = value as Record<string, unknown>; const id = candidate.id; const name = candidate.name; const mimeType = candidate.mimeType; const sizeBytes = candidate.sizeBytes; const dataUrl = candidate.dataUrl; if ( typeof id !== "string" || typeof name !== "string" || typeof mimeType !== "string" || typeof sizeBytes !== "number" || !Number.isFinite(sizeBytes) || typeof dataUrl !== "string" || id.length === 0 || dataUrl.length === 0 ) { return null; } return { id, name, mimeType, sizeBytes, dataUrl, };} function normalizePersistedTerminalContextDraft( value: unknown,): PersistedTerminalContextDraft | null { if (!value || typeof value !== "object") { return null; } const candidate = value as Record<string, unknown>; const id = candidate.id; const threadId = candidate.threadId; const createdAt = candidate.createdAt; const lineStart = candidate.lineStart; const lineEnd = candidate.lineEnd; if ( typeof id !== "string" || id.length === 0 || typeof threadId !== "string" || threadId.length === 0 || typeof createdAt !== "string" || createdAt.length === 0 || typeof lineStart !== "number" || !Number.isFinite(lineStart) || typeof lineEnd !== "number" || !Number.isFinite(lineEnd) ) { return null; } const terminalId = typeof candidate.terminalId === "string" ? candidate.terminalId.trim() : ""; const terminalLabel = typeof candidate.terminalLabel === "string" ? candidate.terminalLabel.trim() : ""; if (terminalId.length === 0 || terminalLabel.length === 0) { return null; } const normalizedLineStart = Math.max(1, Math.floor(lineStart)); const normalizedLineEnd = Math.max(normalizedLineStart, Math.floor(lineEnd)); return { id, threadId: threadId as ThreadId, createdAt, terminalId, terminalLabel, lineStart: normalizedLineStart, lineEnd: normalizedLineEnd, };} function normalizeDraftThreadEnvMode( value: unknown, fallbackWorktreePath: string | null,): DraftThreadEnvMode { if (value === "local" || value === "worktree") { return value; } return fallbackWorktreePath ? "worktree" : "local";} function normalizePersistedDraftThreads( rawDraftThreadsByThreadId: unknown, rawProjectDraftThreadIdByProjectId: unknown,): Pick< PersistedComposerDraftStoreState,Lines 1–500 of 1672. Load more
import { CODEX_REASONING_EFFORT_OPTIONS, type ClaudeCodeEffort, type CodexReasoningEffort, DEFAULT_REASONING_EFFORT_BY_PROVIDER, ProjectId, ProviderInteractionMode, ProviderKind, ProviderModelOptions, RuntimeMode, ThreadId, } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { DeepMutable } from "effect/Types"; import { normalizeModelSlug } from "@t3tools/shared/model"; import { getLocalStorageItem } from "./hooks/useLocalStorage"; import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type ChatImageAttachment } from "./types"; import { type TerminalContextDraft, ensureInlineTerminalContextPlaceholders, normalizeTerminalContextText, } from "./lib/terminalContext"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; import { createDebouncedStorage, createMemoryStorage } from "./lib/storage"; export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"; const COMPOSER_DRAFT_STORAGE_VERSION = 2; const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]); export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type; const COMPOSER_PERSIST_DEBOUNCE_MS = 300; const composerDebouncedStorage = createDebouncedStorage( typeof localStorage !== "undefined" ? localStorage : createMemoryStorage(), COMPOSER_PERSIST_DEBOUNCE_MS, ); // Flush pending composer draft writes before page unload to prevent data loss. if (typeof window !== "undefined") { window.addEventListener("beforeunload", () => { composerDebouncedStorage.flush(); }); } export const PersistedComposerImageAttachment = Schema.Struct({ id: Schema.String, name: Schema.String, mimeType: Schema.String, sizeBytes: Schema.Number, dataUrl: Schema.String, }); export type PersistedComposerImageAttachment = typeof PersistedComposerImageAttachment.Type; export interface ComposerImageAttachment extends Omit<ChatImageAttachment, "previewUrl"> { previewUrl: string; file: File; } const PersistedTerminalContextDraft = Schema.Struct({ id: Schema.String, threadId: ThreadId, createdAt: Schema.String, terminalId: Schema.String, terminalLabel: Schema.String, lineStart: Schema.Number, lineEnd: Schema.Number, }); type PersistedTerminalContextDraft = typeof PersistedTerminalContextDraft.Type; const PersistedComposerThreadDraftState = Schema.Struct({ prompt: Schema.String, attachments: Schema.Array(PersistedComposerImageAttachment), terminalContexts: Schema.optionalKey(Schema.Array(PersistedTerminalContextDraft)), provider: Schema.optionalKey(ProviderKind), model: Schema.optionalKey(Schema.String), modelOptions: Schema.optionalKey(ProviderModelOptions), 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; type LegacyPersistedCodexThreadDraftState = PersistedComposerThreadDraftState & LegacyCodexFields; const PersistedDraftThreadState = Schema.Struct({ projectId: ProjectId, createdAt: Schema.String, runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode, branch: Schema.NullOr(Schema.String), worktreePath: Schema.NullOr(Schema.String), envMode: DraftThreadEnvModeSchema, }); type PersistedDraftThreadState = typeof PersistedDraftThreadState.Type; const PersistedComposerDraftStoreState = Schema.Struct({ draftsByThreadId: Schema.Record(ThreadId, PersistedComposerThreadDraftState), draftThreadsByThreadId: Schema.Record(ThreadId, PersistedDraftThreadState), projectDraftThreadIdByProjectId: Schema.Record(ProjectId, ThreadId), }); type PersistedComposerDraftStoreState = typeof PersistedComposerDraftStoreState.Type; const PersistedComposerDraftStoreStorage = Schema.Struct({ version: Schema.Number, state: PersistedComposerDraftStoreState, }); interface ComposerThreadDraftState { prompt: string; images: ComposerImageAttachment[]; nonPersistedImageIds: string[]; persistedAttachments: PersistedComposerImageAttachment[]; terminalContexts: TerminalContextDraft[]; provider: ProviderKind | null; model: string | null; modelOptions: ProviderModelOptions | null; runtimeMode: RuntimeMode | null; interactionMode: ProviderInteractionMode | null; } export interface DraftThreadState { projectId: ProjectId; createdAt: string; runtimeMode: RuntimeMode; interactionMode: ProviderInteractionMode; branch: string | null; worktreePath: string | null; envMode: DraftThreadEnvMode; } interface ProjectDraftThread extends DraftThreadState { threadId: ThreadId; } interface ComposerDraftStoreState { draftsByThreadId: Record<ThreadId, ComposerThreadDraftState>; draftThreadsByThreadId: Record<ThreadId, DraftThreadState>; projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>; getDraftThreadByProjectId: (projectId: ProjectId) => ProjectDraftThread | null; getDraftThread: (threadId: ThreadId) => DraftThreadState | null; setProjectDraftThreadId: ( projectId: ProjectId, threadId: ThreadId, options?: { branch?: string | null; worktreePath?: string | null; createdAt?: string; envMode?: DraftThreadEnvMode; runtimeMode?: RuntimeMode; interactionMode?: ProviderInteractionMode; }, ) => void; setDraftThreadContext: ( threadId: ThreadId, options: { branch?: string | null; worktreePath?: string | null; projectId?: ProjectId; createdAt?: string; envMode?: DraftThreadEnvMode; runtimeMode?: RuntimeMode; interactionMode?: ProviderInteractionMode; }, ) => void; clearProjectDraftThreadId: (projectId: ProjectId) => void; clearProjectDraftThreadById: (projectId: ProjectId, threadId: ThreadId) => void; clearDraftThread: (threadId: ThreadId) => void; setPrompt: (threadId: ThreadId, prompt: string) => void; setTerminalContexts: (threadId: ThreadId, contexts: TerminalContextDraft[]) => void; setProvider: (threadId: ThreadId, provider: ProviderKind | null | undefined) => void; setModel: (threadId: ThreadId, model: string | null | undefined) => void; setModelOptions: ( threadId: ThreadId, modelOptions: ProviderModelOptions | null | undefined, ) => void; setRuntimeMode: (threadId: ThreadId, runtimeMode: RuntimeMode | null | undefined) => void; setInteractionMode: ( threadId: ThreadId, interactionMode: ProviderInteractionMode | null | undefined, ) => void; addImage: (threadId: ThreadId, image: ComposerImageAttachment) => void; addImages: (threadId: ThreadId, images: ComposerImageAttachment[]) => void; removeImage: (threadId: ThreadId, imageId: string) => void; insertTerminalContext: ( threadId: ThreadId, prompt: string, context: TerminalContextDraft, index: number, ) => boolean; addTerminalContext: (threadId: ThreadId, context: TerminalContextDraft) => void; addTerminalContexts: (threadId: ThreadId, contexts: TerminalContextDraft[]) => void; removeTerminalContext: (threadId: ThreadId, contextId: string) => void; clearTerminalContexts: (threadId: ThreadId) => void; clearPersistedAttachments: (threadId: ThreadId) => void; syncPersistedAttachments: ( threadId: ThreadId, attachments: PersistedComposerImageAttachment[], ) => void; clearComposerContent: (threadId: ThreadId) => void; clearThreadDraft: (threadId: ThreadId) => void; } const EMPTY_PERSISTED_DRAFT_STORE_STATE: PersistedComposerDraftStoreState = { draftsByThreadId: {}, draftThreadsByThreadId: {}, projectDraftThreadIdByProjectId: {}, }; 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_THREAD_DRAFT = Object.freeze<ComposerThreadDraftState>({ prompt: "", images: EMPTY_IMAGES, nonPersistedImageIds: EMPTY_IDS, persistedAttachments: EMPTY_PERSISTED_ATTACHMENTS, terminalContexts: EMPTY_TERMINAL_CONTEXTS, provider: null, model: null, modelOptions: null, runtimeMode: null, interactionMode: null, }); function createEmptyThreadDraft(): ComposerThreadDraftState { return { prompt: "", images: [], nonPersistedImageIds: [], persistedAttachments: [], terminalContexts: [], provider: null, model: null, modelOptions: null, runtimeMode: null, interactionMode: null, }; } function composerImageDedupKey(image: ComposerImageAttachment): string { // Keep this independent from File.lastModified so dedupe is stable for hydrated // images reconstructed from localStorage (which get a fresh lastModified value). return `${image.mimeType}\u0000${image.sizeBytes}\u0000${image.name}`; } function terminalContextDedupKey(context: TerminalContextDraft): string { return `${context.terminalId}\u0000${context.lineStart}\u0000${context.lineEnd}`; } function normalizeTerminalContextForThread( threadId: ThreadId, context: TerminalContextDraft, ): TerminalContextDraft | null { const terminalId = context.terminalId.trim(); const terminalLabel = context.terminalLabel.trim(); if (terminalId.length === 0 || terminalLabel.length === 0) { return null; } const lineStart = Math.max(1, Math.floor(context.lineStart)); const lineEnd = Math.max(lineStart, Math.floor(context.lineEnd)); return { ...context, threadId, terminalId, terminalLabel, lineStart, lineEnd, text: normalizeTerminalContextText(context.text), }; } function normalizeTerminalContextsForThread( threadId: ThreadId, contexts: ReadonlyArray<TerminalContextDraft>, ): TerminalContextDraft[] { const existingIds = new Set<string>(); const existingDedupKeys = new Set<string>(); const normalizedContexts: TerminalContextDraft[] = []; for (const context of contexts) { const normalizedContext = normalizeTerminalContextForThread(threadId, context); if (!normalizedContext) { continue; } const dedupKey = terminalContextDedupKey(normalizedContext); if (existingIds.has(normalizedContext.id) || existingDedupKeys.has(dedupKey)) { continue; } normalizedContexts.push(normalizedContext); existingIds.add(normalizedContext.id); existingDedupKeys.add(dedupKey); } return normalizedContexts; } function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { return ( draft.prompt.length === 0 && draft.images.length === 0 && draft.persistedAttachments.length === 0 && draft.terminalContexts.length === 0 && draft.provider === null && draft.model === null && draft.modelOptions === null && draft.runtimeMode === null && draft.interactionMode === null ); } function normalizeProviderKind(value: unknown): ProviderKind | null { return value === "codex" || value === "claudeAgent" ? value : null; } function normalizeProviderModelOptions( value: unknown, provider?: ProviderKind | null, legacy?: LegacyCodexFields, ): ProviderModelOptions | null { const candidate = value && typeof value === "object" ? (value as Record<string, unknown>) : null; const codexCandidate = candidate?.codex && typeof candidate.codex === "object" ? (candidate.codex as Record<string, unknown>) : null; const claudeCandidate = candidate?.claudeAgent && typeof candidate.claudeAgent === "object" ? (candidate.claudeAgent as Record<string, unknown>) : null; const codexReasoningEffort: CodexReasoningEffort | undefined = codexCandidate?.reasoningEffort === "low" || codexCandidate?.reasoningEffort === "medium" || codexCandidate?.reasoningEffort === "high" || codexCandidate?.reasoningEffort === "xhigh" ? codexCandidate.reasoningEffort : provider === "codex" && (legacy?.effort === "low" || legacy?.effort === "medium" || legacy?.effort === "high" || legacy?.effort === "xhigh") ? legacy.effort : undefined; const codexFastMode = codexCandidate?.fastMode === true || (provider === "codex" && legacy?.codexFastMode === true) || (typeof legacy?.serviceTier === "string" && legacy.serviceTier === "fast"); const codex = codexReasoningEffort && codexReasoningEffort !== DEFAULT_REASONING_EFFORT_BY_PROVIDER.codex ? { reasoningEffort: codexReasoningEffort, ...(codexFastMode ? { fastMode: true } : {}), } : codexFastMode ? { fastMode: true } : undefined; const claudeThinking = claudeCandidate?.thinking === false ? false : undefined; const claudeEffort: ClaudeCodeEffort | undefined = claudeCandidate?.effort === "low" || claudeCandidate?.effort === "medium" || claudeCandidate?.effort === "high" || claudeCandidate?.effort === "max" || claudeCandidate?.effort === "ultrathink" ? claudeCandidate.effort : undefined; const claudeFastMode = claudeCandidate?.fastMode === true; const claude = claudeThinking === false || (claudeEffort && claudeEffort !== DEFAULT_REASONING_EFFORT_BY_PROVIDER.claudeAgent) || claudeFastMode ? { ...(claudeThinking === false ? { thinking: false } : {}), ...(claudeEffort && claudeEffort !== DEFAULT_REASONING_EFFORT_BY_PROVIDER.claudeAgent ? { effort: claudeEffort } : {}), ...(claudeFastMode ? { fastMode: true } : {}), } : undefined; if (!codex && !claude) { return null; } return { ...(codex ? { codex } : {}), ...(claude ? { claudeAgent: claude } : {}), }; } function revokeObjectPreviewUrl(previewUrl: string): void { if (typeof URL === "undefined") { return; } if (!previewUrl.startsWith("blob:")) { return; } URL.revokeObjectURL(previewUrl); } function normalizePersistedAttachment(value: unknown): PersistedComposerImageAttachment | null { if (!value || typeof value !== "object") { return null; } const candidate = value as Record<string, unknown>; const id = candidate.id; const name = candidate.name; const mimeType = candidate.mimeType; const sizeBytes = candidate.sizeBytes; const dataUrl = candidate.dataUrl; if ( typeof id !== "string" || typeof name !== "string" || typeof mimeType !== "string" || typeof sizeBytes !== "number" || !Number.isFinite(sizeBytes) || typeof dataUrl !== "string" || id.length === 0 || dataUrl.length === 0 ) { return null; } return { id, name, mimeType, sizeBytes, dataUrl, }; } function normalizePersistedTerminalContextDraft( value: unknown, ): PersistedTerminalContextDraft | null { if (!value || typeof value !== "object") { return null; } const candidate = value as Record<string, unknown>; const id = candidate.id; const threadId = candidate.threadId; const createdAt = candidate.createdAt; const lineStart = candidate.lineStart; const lineEnd = candidate.lineEnd; if ( typeof id !== "string" || id.length === 0 || typeof threadId !== "string" || threadId.length === 0 || typeof createdAt !== "string" || createdAt.length === 0 || typeof lineStart !== "number" || !Number.isFinite(lineStart) || typeof lineEnd !== "number" || !Number.isFinite(lineEnd) ) { return null; } const terminalId = typeof candidate.terminalId === "string" ? candidate.terminalId.trim() : ""; const terminalLabel = typeof candidate.terminalLabel === "string" ? candidate.terminalLabel.trim() : ""; if (terminalId.length === 0 || terminalLabel.length === 0) { return null; } const normalizedLineStart = Math.max(1, Math.floor(lineStart)); const normalizedLineEnd = Math.max(normalizedLineStart, Math.floor(lineEnd)); return { id, threadId: threadId as ThreadId, createdAt, terminalId, terminalLabel, lineStart: normalizedLineStart, lineEnd: normalizedLineEnd, }; } function normalizeDraftThreadEnvMode( value: unknown, fallbackWorktreePath: string | null, ): DraftThreadEnvMode { if (value === "local" || value === "worktree") { return value; } return fallbackWorktreePath ? "worktree" : "local"; } function normalizePersistedDraftThreads( rawDraftThreadsByThreadId: unknown, rawProjectDraftThreadIdByProjectId: unknown, ): Pick< PersistedComposerDraftStoreState,