import { DEFAULT_REASONING_EFFORT_BY_PROVIDER, ProjectId, REASONING_EFFORT_OPTIONS_BY_PROVIDER, ThreadId, type CodexReasoningEffort, type ProviderKind, type ProviderInteractionMode, type RuntimeMode,} from "@t3tools/contracts";import { normalizeModelSlug } from "@t3tools/shared/model";import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type ChatImageAttachment } from "./types";import { type TerminalContextDraft, ensureInlineTerminalContextPlaceholders, normalizeTerminalContextText,} from "./lib/terminalContext";import { Debouncer } from "@tanstack/react-pacer";import { create } from "zustand";import { createJSONStorage, persist, type StateStorage } from "zustand/middleware"; export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";export type DraftThreadEnvMode = "local" | "worktree"; const COMPOSER_PERSIST_DEBOUNCE_MS = 300; interface DebouncedStorage extends StateStorage { flush: () => void;} export function createDebouncedStorage(baseStorage: StateStorage): DebouncedStorage { const debouncedSetItem = new Debouncer( (name: string, value: string) => { baseStorage.setItem(name, value); }, { wait: COMPOSER_PERSIST_DEBOUNCE_MS }, ); return { getItem: (name) => baseStorage.getItem(name), setItem: (name, value) => { debouncedSetItem.maybeExecute(name, value); }, removeItem: (name) => { debouncedSetItem.cancel(); baseStorage.removeItem(name); }, flush: () => { debouncedSetItem.flush(); }, };} const composerDebouncedStorage: DebouncedStorage = typeof localStorage !== "undefined" ? createDebouncedStorage(localStorage) : { getItem: () => null, setItem: () => {}, removeItem: () => {}, flush: () => {} }; // Flush pending composer draft writes before page unload to prevent data loss.if (typeof window !== "undefined") { window.addEventListener("beforeunload", () => { composerDebouncedStorage.flush(); });} export interface PersistedComposerImageAttachment { id: string; name: string; mimeType: string; sizeBytes: number; dataUrl: string;} export interface ComposerImageAttachment extends Omit<ChatImageAttachment, "previewUrl"> { previewUrl: string; file: File;} interface PersistedTerminalContextDraft { id: string; threadId: ThreadId; createdAt: string; terminalId: string; terminalLabel: string; lineStart: number; lineEnd: number;} interface PersistedComposerThreadDraftState { prompt: string; attachments: PersistedComposerImageAttachment[]; terminalContexts?: PersistedTerminalContextDraft[]; provider?: ProviderKind | null; model?: string | null; runtimeMode?: RuntimeMode | null; interactionMode?: ProviderInteractionMode | null; effort?: CodexReasoningEffort | null; codexFastMode?: boolean | null; serviceTier?: string | null;} interface PersistedDraftThreadState { projectId: ProjectId; createdAt: string; runtimeMode: RuntimeMode; interactionMode: ProviderInteractionMode; branch: string | null; worktreePath: string | null; envMode: DraftThreadEnvMode;} interface PersistedComposerDraftStoreState { draftsByThreadId: Record<ThreadId, PersistedComposerThreadDraftState>; draftThreadsByThreadId: Record<ThreadId, PersistedDraftThreadState>; projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>;} interface ComposerThreadDraftState { prompt: string; images: ComposerImageAttachment[]; nonPersistedImageIds: string[]; persistedAttachments: PersistedComposerImageAttachment[]; terminalContexts: TerminalContextDraft[]; provider: ProviderKind | null; model: string | null; runtimeMode: RuntimeMode | null; interactionMode: ProviderInteractionMode | null; effort: CodexReasoningEffort | null; codexFastMode: boolean;} 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; setRuntimeMode: (threadId: ThreadId, runtimeMode: RuntimeMode | null | undefined) => void; setInteractionMode: ( threadId: ThreadId, interactionMode: ProviderInteractionMode | null | undefined, ) => void; setEffort: (threadId: ThreadId, effort: CodexReasoningEffort | null | undefined) => void; setCodexFastMode: (threadId: ThreadId, enabled: boolean | 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);Object.freeze(EMPTY_TERMINAL_CONTEXTS);const EMPTY_THREAD_DRAFT = Object.freeze({ prompt: "", images: EMPTY_IMAGES, nonPersistedImageIds: EMPTY_IDS, persistedAttachments: EMPTY_PERSISTED_ATTACHMENTS, terminalContexts: EMPTY_TERMINAL_CONTEXTS, provider: null, model: null, runtimeMode: null, interactionMode: null, effort: null, codexFastMode: false,}) as ComposerThreadDraftState; const REASONING_EFFORT_VALUES = new Set<CodexReasoningEffort>( REASONING_EFFORT_OPTIONS_BY_PROVIDER.codex,); function createEmptyThreadDraft(): ComposerThreadDraftState { return { prompt: "", images: [], nonPersistedImageIds: [], persistedAttachments: [], terminalContexts: [], provider: null, model: null, runtimeMode: null, interactionMode: null, effort: null, codexFastMode: false, };} 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.runtimeMode === null && draft.interactionMode === null && draft.effort === null && draft.codexFastMode === false );} function normalizeProviderKind(value: unknown): ProviderKind | null { return value === "codex" ? value : null;} 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 normalizePersistedComposerDraftState(value: unknown): PersistedComposerDraftStoreState { if (!value || typeof value !== "object") { return EMPTY_PERSISTED_DRAFT_STORE_STATE; } const candidate = value as Record<string, unknown>; const rawDraftMap = candidate.draftsByThreadId; const rawDraftThreadsByThreadId = candidate.draftThreadsByThreadId; const rawProjectDraftThreadIdByProjectId = candidate.projectDraftThreadIdByProjectId; const draftThreadsByThreadId: PersistedComposerDraftStoreState["draftThreadsByThreadId"] = {}; if (rawDraftThreadsByThreadId && typeof rawDraftThreadsByThreadId === "object") { for (const [threadId, rawDraftThread] of Object.entries( rawDraftThreadsByThreadId as Record<string, unknown>, )) { if (typeof threadId !== "string" || threadId.length === 0) { continue; } if (!rawDraftThread || typeof rawDraftThread !== "object") { continue; } const candidateDraftThread = rawDraftThread as Record<string, unknown>; const projectId = candidateDraftThread.projectId; const createdAt = candidateDraftThread.createdAt; const branch = candidateDraftThread.branch; const worktreePath = candidateDraftThread.worktreePath; const normalizedWorktreePath = typeof worktreePath === "string" ? worktreePath : null; if (typeof projectId !== "string" || projectId.length === 0) { continue; } draftThreadsByThreadId[threadId as ThreadId] = { projectId: projectId as ProjectId, createdAt: typeof createdAt === "string" && createdAt.length > 0 ? createdAt : new Date().toISOString(), runtimeMode: candidateDraftThread.runtimeMode === "approval-required" || candidateDraftThread.runtimeMode === "full-access" ? candidateDraftThread.runtimeMode : DEFAULT_RUNTIME_MODE, interactionMode: candidateDraftThread.interactionMode === "plan" || candidateDraftThread.interactionMode === "default" ? candidateDraftThread.interactionMode : DEFAULT_INTERACTION_MODE, branch: typeof branch === "string" ? branch : null, worktreePath: normalizedWorktreePath, envMode: normalizeDraftThreadEnvMode(candidateDraftThread.envMode, normalizedWorktreePath), }; } } const projectDraftThreadIdByProjectId: PersistedComposerDraftStoreState["projectDraftThreadIdByProjectId"] = {}; if ( rawProjectDraftThreadIdByProjectId && typeof rawProjectDraftThreadIdByProjectId === "object" ) { for (const [projectId, threadId] of Object.entries( rawProjectDraftThreadIdByProjectId as Record<string, unknown>, )) { if ( typeof projectId === "string" && projectId.length > 0 && typeof threadId === "string" && threadId.length > 0 ) { projectDraftThreadIdByProjectId[projectId as ProjectId] = threadId as ThreadId; if (!draftThreadsByThreadId[threadId as ThreadId]) { draftThreadsByThreadId[threadId as ThreadId] = { projectId: projectId as ProjectId,Lines 1–500 of 1599. Load more
import { DEFAULT_REASONING_EFFORT_BY_PROVIDER, ProjectId, REASONING_EFFORT_OPTIONS_BY_PROVIDER, ThreadId, type CodexReasoningEffort, type ProviderKind, type ProviderInteractionMode, type RuntimeMode, } from "@t3tools/contracts"; import { normalizeModelSlug } from "@t3tools/shared/model"; import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type ChatImageAttachment } from "./types"; import { type TerminalContextDraft, ensureInlineTerminalContextPlaceholders, normalizeTerminalContextText, } from "./lib/terminalContext"; import { Debouncer } from "@tanstack/react-pacer"; import { create } from "zustand"; import { createJSONStorage, persist, type StateStorage } from "zustand/middleware"; export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"; export type DraftThreadEnvMode = "local" | "worktree"; const COMPOSER_PERSIST_DEBOUNCE_MS = 300; interface DebouncedStorage extends StateStorage { flush: () => void; } export function createDebouncedStorage(baseStorage: StateStorage): DebouncedStorage { const debouncedSetItem = new Debouncer( (name: string, value: string) => { baseStorage.setItem(name, value); }, { wait: COMPOSER_PERSIST_DEBOUNCE_MS }, ); return { getItem: (name) => baseStorage.getItem(name), setItem: (name, value) => { debouncedSetItem.maybeExecute(name, value); }, removeItem: (name) => { debouncedSetItem.cancel(); baseStorage.removeItem(name); }, flush: () => { debouncedSetItem.flush(); }, }; } const composerDebouncedStorage: DebouncedStorage = typeof localStorage !== "undefined" ? createDebouncedStorage(localStorage) : { getItem: () => null, setItem: () => {}, removeItem: () => {}, flush: () => {} }; // Flush pending composer draft writes before page unload to prevent data loss. if (typeof window !== "undefined") { window.addEventListener("beforeunload", () => { composerDebouncedStorage.flush(); }); } export interface PersistedComposerImageAttachment { id: string; name: string; mimeType: string; sizeBytes: number; dataUrl: string; } export interface ComposerImageAttachment extends Omit<ChatImageAttachment, "previewUrl"> { previewUrl: string; file: File; } interface PersistedTerminalContextDraft { id: string; threadId: ThreadId; createdAt: string; terminalId: string; terminalLabel: string; lineStart: number; lineEnd: number; } interface PersistedComposerThreadDraftState { prompt: string; attachments: PersistedComposerImageAttachment[]; terminalContexts?: PersistedTerminalContextDraft[]; provider?: ProviderKind | null; model?: string | null; runtimeMode?: RuntimeMode | null; interactionMode?: ProviderInteractionMode | null; effort?: CodexReasoningEffort | null; codexFastMode?: boolean | null; serviceTier?: string | null; } interface PersistedDraftThreadState { projectId: ProjectId; createdAt: string; runtimeMode: RuntimeMode; interactionMode: ProviderInteractionMode; branch: string | null; worktreePath: string | null; envMode: DraftThreadEnvMode; } interface PersistedComposerDraftStoreState { draftsByThreadId: Record<ThreadId, PersistedComposerThreadDraftState>; draftThreadsByThreadId: Record<ThreadId, PersistedDraftThreadState>; projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>; } interface ComposerThreadDraftState { prompt: string; images: ComposerImageAttachment[]; nonPersistedImageIds: string[]; persistedAttachments: PersistedComposerImageAttachment[]; terminalContexts: TerminalContextDraft[]; provider: ProviderKind | null; model: string | null; runtimeMode: RuntimeMode | null; interactionMode: ProviderInteractionMode | null; effort: CodexReasoningEffort | null; codexFastMode: boolean; } 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; setRuntimeMode: (threadId: ThreadId, runtimeMode: RuntimeMode | null | undefined) => void; setInteractionMode: ( threadId: ThreadId, interactionMode: ProviderInteractionMode | null | undefined, ) => void; setEffort: (threadId: ThreadId, effort: CodexReasoningEffort | null | undefined) => void; setCodexFastMode: (threadId: ThreadId, enabled: boolean | 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); Object.freeze(EMPTY_TERMINAL_CONTEXTS); const EMPTY_THREAD_DRAFT = Object.freeze({ prompt: "", images: EMPTY_IMAGES, nonPersistedImageIds: EMPTY_IDS, persistedAttachments: EMPTY_PERSISTED_ATTACHMENTS, terminalContexts: EMPTY_TERMINAL_CONTEXTS, provider: null, model: null, runtimeMode: null, interactionMode: null, effort: null, codexFastMode: false, }) as ComposerThreadDraftState; const REASONING_EFFORT_VALUES = new Set<CodexReasoningEffort>( REASONING_EFFORT_OPTIONS_BY_PROVIDER.codex, ); function createEmptyThreadDraft(): ComposerThreadDraftState { return { prompt: "", images: [], nonPersistedImageIds: [], persistedAttachments: [], terminalContexts: [], provider: null, model: null, runtimeMode: null, interactionMode: null, effort: null, codexFastMode: false, }; } 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.runtimeMode === null && draft.interactionMode === null && draft.effort === null && draft.codexFastMode === false ); } function normalizeProviderKind(value: unknown): ProviderKind | null { return value === "codex" ? value : null; } 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 normalizePersistedComposerDraftState(value: unknown): PersistedComposerDraftStoreState { if (!value || typeof value !== "object") { return EMPTY_PERSISTED_DRAFT_STORE_STATE; } const candidate = value as Record<string, unknown>; const rawDraftMap = candidate.draftsByThreadId; const rawDraftThreadsByThreadId = candidate.draftThreadsByThreadId; const rawProjectDraftThreadIdByProjectId = candidate.projectDraftThreadIdByProjectId; const draftThreadsByThreadId: PersistedComposerDraftStoreState["draftThreadsByThreadId"] = {}; if (rawDraftThreadsByThreadId && typeof rawDraftThreadsByThreadId === "object") { for (const [threadId, rawDraftThread] of Object.entries( rawDraftThreadsByThreadId as Record<string, unknown>, )) { if (typeof threadId !== "string" || threadId.length === 0) { continue; } if (!rawDraftThread || typeof rawDraftThread !== "object") { continue; } const candidateDraftThread = rawDraftThread as Record<string, unknown>; const projectId = candidateDraftThread.projectId; const createdAt = candidateDraftThread.createdAt; const branch = candidateDraftThread.branch; const worktreePath = candidateDraftThread.worktreePath; const normalizedWorktreePath = typeof worktreePath === "string" ? worktreePath : null; if (typeof projectId !== "string" || projectId.length === 0) { continue; } draftThreadsByThreadId[threadId as ThreadId] = { projectId: projectId as ProjectId, createdAt: typeof createdAt === "string" && createdAt.length > 0 ? createdAt : new Date().toISOString(), runtimeMode: candidateDraftThread.runtimeMode === "approval-required" || candidateDraftThread.runtimeMode === "full-access" ? candidateDraftThread.runtimeMode : DEFAULT_RUNTIME_MODE, interactionMode: candidateDraftThread.interactionMode === "plan" || candidateDraftThread.interactionMode === "default" ? candidateDraftThread.interactionMode : DEFAULT_INTERACTION_MODE, branch: typeof branch === "string" ? branch : null, worktreePath: normalizedWorktreePath, envMode: normalizeDraftThreadEnvMode(candidateDraftThread.envMode, normalizedWorktreePath), }; } } const projectDraftThreadIdByProjectId: PersistedComposerDraftStoreState["projectDraftThreadIdByProjectId"] = {}; if ( rawProjectDraftThreadIdByProjectId && typeof rawProjectDraftThreadIdByProjectId === "object" ) { for (const [projectId, threadId] of Object.entries( rawProjectDraftThreadIdByProjectId as Record<string, unknown>, )) { if ( typeof projectId === "string" && projectId.length > 0 && typeof threadId === "string" && threadId.length > 0 ) { projectDraftThreadIdByProjectId[projectId as ProjectId] = threadId as ThreadId; if (!draftThreadsByThreadId[threadId as ThreadId]) { draftThreadsByThreadId[threadId as ThreadId] = { projectId: projectId as ProjectId,