| ... | @@ -8,43 +8,10 @@ import { | ... | @@ -8,43 +8,10 @@ import { |
| 8 | } from "@t3tools/contracts"; | 8 | } from "@t3tools/contracts"; |
| 9 | import type { ChatImageAttachment } from "./types"; | 9 | import type { ChatImageAttachment } from "./types"; |
| 10 | import { create } from "zustand"; | 10 | import { create } from "zustand"; |
| 11 | import { createJSONStorage, persist, type StateStorage } from "zustand/middleware"; | 11 | import { createJSONStorage, persist } from "zustand/middleware"; |
| 12 | | 12 | |
| 13 | export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"; | 13 | export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"; |
| 14 | | 14 | |
| 15 | const SAFE_LOCAL_STORAGE: StateStorage = { | | |
| 16 | getItem: (key) => { | | |
| 17 | if (typeof window === "undefined") { | | |
| 18 | return null; | | |
| 19 | } | | |
| 20 | try { | | |
| 21 | return window.localStorage.getItem(key); | | |
| 22 | } catch { | | |
| 23 | return null; | | |
| 24 | } | | |
| 25 | }, | | |
| 26 | setItem: (key, value) => { | | |
| 27 | if (typeof window === "undefined") { | | |
| 28 | return; | | |
| 29 | } | | |
| 30 | try { | | |
| 31 | window.localStorage.setItem(key, value); | | |
| 32 | } catch { | | |
| 33 | // Swallow quota/storage errors so draft state stays usable in-memory. | | |
| 34 | } | | |
| 35 | }, | | |
| 36 | removeItem: (key) => { | | |
| 37 | if (typeof window === "undefined") { | | |
| 38 | return; | | |
| 39 | } | | |
| 40 | try { | | |
| 41 | window.localStorage.removeItem(key); | | |
| 42 | } catch { | | |
| 43 | // Best-effort cleanup. | | |
| 44 | } | | |
| 45 | }, | | |
| 46 | }; | | |
| 47 | | | |
| 48 | export interface PersistedComposerImageAttachment { | 15 | export interface PersistedComposerImageAttachment { |
| 49 | id: string; | 16 | id: string; |
| 50 | name: string; | 17 | name: string; |
| ... | @@ -79,7 +46,7 @@ interface PersistedComposerDraftStoreState { | ... | @@ -79,7 +46,7 @@ interface PersistedComposerDraftStoreState { |
| 79 | projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>; | 46 | projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>; |
| 80 | } | 47 | } |
| 81 | | 48 | |
| 82 | export interface ComposerThreadDraftState { | 49 | interface ComposerThreadDraftState { |
| 83 | prompt: string; | 50 | prompt: string; |
| 84 | cursor: number; | 51 | cursor: number; |
| 85 | images: ComposerImageAttachment[]; | 52 | images: ComposerImageAttachment[]; |
| ... | @@ -96,13 +63,16 @@ export interface DraftThreadState { | ... | @@ -96,13 +63,16 @@ export interface DraftThreadState { |
| 96 | worktreePath: string | null; | 63 | worktreePath: string | null; |
| 97 | } | 64 | } |
| 98 | | 65 | |
| | 66 | interface ProjectDraftThread extends DraftThreadState { |
| | 67 | threadId: ThreadId; |
| | 68 | } |
| | 69 | |
| 99 | interface ComposerDraftStoreState { | 70 | interface ComposerDraftStoreState { |
| 100 | draftsByThreadId: Record<ThreadId, ComposerThreadDraftState>; | 71 | draftsByThreadId: Record<ThreadId, ComposerThreadDraftState>; |
| 101 | draftThreadsByThreadId: Record<ThreadId, DraftThreadState>; | 72 | draftThreadsByThreadId: Record<ThreadId, DraftThreadState>; |
| 102 | projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>; | 73 | projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>; |
| 103 | readDraftThread: (threadId: ThreadId) => DraftThreadState | null; | 74 | getDraftThreadByProjectId: (projectId: ProjectId) => ProjectDraftThread | null; |
| 104 | isDraftThread: (threadId: ThreadId) => boolean; | 75 | getDraftThread: (threadId: ThreadId) => DraftThreadState | null; |
| 105 | readProjectDraftThreadId: (projectId: ProjectId) => ThreadId | null; | | |
| 106 | setProjectDraftThreadId: ( | 76 | setProjectDraftThreadId: ( |
| 107 | projectId: ProjectId, | 77 | projectId: ProjectId, |
| 108 | threadId: ThreadId, | 78 | threadId: ThreadId, |
| ... | @@ -132,11 +102,23 @@ interface ComposerDraftStoreState { | ... | @@ -132,11 +102,23 @@ interface ComposerDraftStoreState { |
| 132 | addImages: (threadId: ThreadId, images: ComposerImageAttachment[]) => void; | 102 | addImages: (threadId: ThreadId, images: ComposerImageAttachment[]) => void; |
| 133 | removeImage: (threadId: ThreadId, imageId: string) => void; | 103 | removeImage: (threadId: ThreadId, imageId: string) => void; |
| 134 | clearPersistedAttachments: (threadId: ThreadId) => void; | 104 | clearPersistedAttachments: (threadId: ThreadId) => void; |
| 135 | syncPersistedAttachments: (threadId: ThreadId, attachments: PersistedComposerImageAttachment[]) => void; | 105 | syncPersistedAttachments: ( |
| | 106 | threadId: ThreadId, |
| | 107 | attachments: PersistedComposerImageAttachment[], |
| | 108 | ) => void; |
| 136 | clearComposerContent: (threadId: ThreadId) => void; | 109 | clearComposerContent: (threadId: ThreadId) => void; |
| 137 | clearThreadDraft: (threadId: ThreadId) => void; | 110 | clearThreadDraft: (threadId: ThreadId) => void; |
| 138 | } | 111 | } |
| 139 | | 112 | |
| | 113 | const EMPTY_PERSISTED_DRAFT_STORE_STATE: PersistedComposerDraftStoreState = { |
| | 114 | draftsByThreadId: {}, |
| | 115 | draftThreadsByThreadId: {}, |
| | 116 | projectDraftThreadIdByProjectId: {}, |
| | 117 | }; |
| | 118 | |
| | 119 | let persistedDraftStateCache: PersistedComposerDraftStoreState = EMPTY_PERSISTED_DRAFT_STORE_STATE; |
| | 120 | let persistedDraftStorageChars: number | null = null; |
| | 121 | |
| 140 | const EMPTY_IMAGES: ComposerImageAttachment[] = []; | 122 | const EMPTY_IMAGES: ComposerImageAttachment[] = []; |
| 141 | const EMPTY_IDS: string[] = []; | 123 | const EMPTY_IDS: string[] = []; |
| 142 | const EMPTY_PERSISTED_ATTACHMENTS: PersistedComposerImageAttachment[] = []; | 124 | const EMPTY_PERSISTED_ATTACHMENTS: PersistedComposerImageAttachment[] = []; |
| ... | @@ -223,7 +205,7 @@ function normalizePersistedAttachment(value: unknown): PersistedComposerImageAtt | ... | @@ -223,7 +205,7 @@ function normalizePersistedAttachment(value: unknown): PersistedComposerImageAtt |
| 223 | | 205 | |
| 224 | function normalizePersistedComposerDraftState(value: unknown): PersistedComposerDraftStoreState { | 206 | function normalizePersistedComposerDraftState(value: unknown): PersistedComposerDraftStoreState { |
| 225 | if (!value || typeof value !== "object") { | 207 | if (!value || typeof value !== "object") { |
| 226 | return { draftsByThreadId: {}, draftThreadsByThreadId: {}, projectDraftThreadIdByProjectId: {} }; | 208 | return EMPTY_PERSISTED_DRAFT_STORE_STATE; |
| 227 | } | 209 | } |
| 228 | const candidate = value as Record<string, unknown>; | 210 | const candidate = value as Record<string, unknown>; |
| 229 | const rawDraftMap = candidate.draftsByThreadId; | 211 | const rawDraftMap = candidate.draftsByThreadId; |
| ... | @@ -250,7 +232,10 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer | ... | @@ -250,7 +232,10 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer |
| 250 | } | 232 | } |
| 251 | draftThreadsByThreadId[threadId as ThreadId] = { | 233 | draftThreadsByThreadId[threadId as ThreadId] = { |
| 252 | projectId: projectId as ProjectId, | 234 | projectId: projectId as ProjectId, |
| 253 | createdAt: typeof createdAt === "string" && createdAt.length > 0 ? createdAt : new Date().toISOString(), | 235 | createdAt: |
| | 236 | typeof createdAt === "string" && createdAt.length > 0 |
| | 237 | ? createdAt |
| | 238 | : new Date().toISOString(), |
| 254 | branch: typeof branch === "string" ? branch : null, | 239 | branch: typeof branch === "string" ? branch : null, |
| 255 | worktreePath: typeof worktreePath === "string" ? worktreePath : null, | 240 | worktreePath: typeof worktreePath === "string" ? worktreePath : null, |
| 256 | }; | 241 | }; |
| ... | @@ -258,7 +243,10 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer | ... | @@ -258,7 +243,10 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer |
| 258 | } | 243 | } |
| 259 | const projectDraftThreadIdByProjectId: PersistedComposerDraftStoreState["projectDraftThreadIdByProjectId"] = | 244 | const projectDraftThreadIdByProjectId: PersistedComposerDraftStoreState["projectDraftThreadIdByProjectId"] = |
| 260 | {}; | 245 | {}; |
| 261 | if (rawProjectDraftThreadIdByProjectId && typeof rawProjectDraftThreadIdByProjectId === "object") { | 246 | if ( |
| | 247 | rawProjectDraftThreadIdByProjectId && |
| | 248 | typeof rawProjectDraftThreadIdByProjectId === "object" |
| | 249 | ) { |
| 262 | for (const [projectId, threadId] of Object.entries( | 250 | for (const [projectId, threadId] of Object.entries( |
| 263 | rawProjectDraftThreadIdByProjectId as Record<string, unknown>, | 251 | rawProjectDraftThreadIdByProjectId as Record<string, unknown>, |
| 264 | )) { | 252 | )) { |
| ... | @@ -327,7 +315,35 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer | ... | @@ -327,7 +315,35 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer |
| 327 | ...(effort ? { effort } : {}), | 315 | ...(effort ? { effort } : {}), |
| 328 | }; | 316 | }; |
| 329 | } | 317 | } |
| 330 | return { draftsByThreadId: nextDraftsByThreadId, draftThreadsByThreadId, projectDraftThreadIdByProjectId }; | 318 | return { |
| | 319 | draftsByThreadId: nextDraftsByThreadId, |
| | 320 | draftThreadsByThreadId, |
| | 321 | projectDraftThreadIdByProjectId, |
| | 322 | }; |
| | 323 | } |
| | 324 | |
| | 325 | function parsePersistedDraftStateRaw(raw: string | null): PersistedComposerDraftStoreState { |
| | 326 | if (!raw) { |
| | 327 | return EMPTY_PERSISTED_DRAFT_STORE_STATE; |
| | 328 | } |
| | 329 | try { |
| | 330 | const parsed = JSON.parse(raw) as unknown; |
| | 331 | if (parsed && typeof parsed === "object" && "state" in parsed) { |
| | 332 | return normalizePersistedComposerDraftState((parsed as { state?: unknown }).state); |
| | 333 | } |
| | 334 | return normalizePersistedComposerDraftState(parsed); |
| | 335 | } catch { |
| | 336 | return EMPTY_PERSISTED_DRAFT_STORE_STATE; |
| | 337 | } |
| | 338 | } |
| | 339 | |
| | 340 | function readPersistedAttachmentIdsFromCache(threadId: ThreadId): string[] { |
| | 341 | if (threadId.length === 0) { |
| | 342 | return []; |
| | 343 | } |
| | 344 | return (persistedDraftStateCache.draftsByThreadId[threadId]?.attachments ?? []).map( |
| | 345 | (attachment) => attachment.id, |
| | 346 | ); |
| 331 | } | 347 | } |
| 332 | | 348 | |
| 333 | function fileFromDataUrl(dataUrl: string, name: string, mimeType: string): File | null { | 349 | function fileFromDataUrl(dataUrl: string, name: string, mimeType: string): File | null { |
| ... | @@ -379,25 +395,6 @@ function hydrateImagesFromPersisted( | ... | @@ -379,25 +395,6 @@ function hydrateImagesFromPersisted( |
| 379 | }); | 395 | }); |
| 380 | } | 396 | } |
| 381 | | 397 | |
| 382 | function readPersistedDraftStateFromLocalStorage(): PersistedComposerDraftStoreState { | | |
| 383 | if (typeof window === "undefined") { | | |
| 384 | return { draftsByThreadId: {}, draftThreadsByThreadId: {}, projectDraftThreadIdByProjectId: {} }; | | |
| 385 | } | | |
| 386 | try { | | |
| 387 | const raw = window.localStorage.getItem(COMPOSER_DRAFT_STORAGE_KEY); | | |
| 388 | if (!raw) { | | |
| 389 | return { draftsByThreadId: {}, draftThreadsByThreadId: {}, projectDraftThreadIdByProjectId: {} }; | | |
| 390 | } | | |
| 391 | const parsed = JSON.parse(raw) as unknown; | | |
| 392 | if (parsed && typeof parsed === "object" && "state" in parsed) { | | |
| 393 | return normalizePersistedComposerDraftState((parsed as { state?: unknown }).state); | | |
| 394 | } | | |
| 395 | return normalizePersistedComposerDraftState(parsed); | | |
| 396 | } catch { | | |
| 397 | return { draftsByThreadId: {}, draftThreadsByThreadId: {}, projectDraftThreadIdByProjectId: {} }; | | |
| 398 | } | | |
| 399 | } | | |
| 400 | | | |
| 401 | function toHydratedThreadDraft( | 398 | function toHydratedThreadDraft( |
| 402 | persistedDraft: PersistedComposerThreadDraftState, | 399 | persistedDraft: PersistedComposerThreadDraftState, |
| 403 | ): ComposerThreadDraftState { | 400 | ): ComposerThreadDraftState { |
| ... | @@ -412,29 +409,80 @@ function toHydratedThreadDraft( | ... | @@ -412,29 +409,80 @@ function toHydratedThreadDraft( |
| 412 | }; | 409 | }; |
| 413 | } | 410 | } |
| 414 | | 411 | |
| | 412 | const composerDraftStorage = createJSONStorage(() => ({ |
| | 413 | getItem: (name: string): string | null => { |
| | 414 | if (typeof window === "undefined") { |
| | 415 | persistedDraftStateCache = EMPTY_PERSISTED_DRAFT_STORE_STATE; |
| | 416 | persistedDraftStorageChars = null; |
| | 417 | return null; |
| | 418 | } |
| | 419 | try { |
| | 420 | const raw = window.localStorage.getItem(name); |
| | 421 | persistedDraftStateCache = parsePersistedDraftStateRaw(raw); |
| | 422 | persistedDraftStorageChars = raw?.length ?? 0; |
| | 423 | return raw; |
| | 424 | } catch { |
| | 425 | persistedDraftStateCache = EMPTY_PERSISTED_DRAFT_STORE_STATE; |
| | 426 | persistedDraftStorageChars = null; |
| | 427 | return null; |
| | 428 | } |
| | 429 | }, |
| | 430 | setItem: (name: string, value: string): void => { |
| | 431 | if (typeof window === "undefined") { |
| | 432 | return; |
| | 433 | } |
| | 434 | try { |
| | 435 | window.localStorage.setItem(name, value); |
| | 436 | persistedDraftStateCache = parsePersistedDraftStateRaw(value); |
| | 437 | persistedDraftStorageChars = value.length; |
| | 438 | } catch { |
| | 439 | // Quota/storage errors are expected for large drafts. Keep last successful snapshot. |
| | 440 | } |
| | 441 | }, |
| | 442 | removeItem: (name: string): void => { |
| | 443 | if (typeof window === "undefined") { |
| | 444 | persistedDraftStateCache = EMPTY_PERSISTED_DRAFT_STORE_STATE; |
| | 445 | persistedDraftStorageChars = null; |
| | 446 | return; |
| | 447 | } |
| | 448 | try { |
| | 449 | window.localStorage.removeItem(name); |
| | 450 | } catch { |
| | 451 | // Ignore remove failures; clear in-memory cache regardless. |
| | 452 | } |
| | 453 | persistedDraftStateCache = EMPTY_PERSISTED_DRAFT_STORE_STATE; |
| | 454 | persistedDraftStorageChars = 0; |
| | 455 | }, |
| | 456 | })); |
| | 457 | |
| 415 | export const useComposerDraftStore = create<ComposerDraftStoreState>()( | 458 | export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 416 | persist( | 459 | persist( |
| 417 | (set, get) => ({ | 460 | (set, get) => ({ |
| 418 | draftsByThreadId: {}, | 461 | draftsByThreadId: {}, |
| 419 | draftThreadsByThreadId: {}, | 462 | draftThreadsByThreadId: {}, |
| 420 | projectDraftThreadIdByProjectId: {}, | 463 | projectDraftThreadIdByProjectId: {}, |
| 421 | readDraftThread: (threadId) => { | 464 | getDraftThreadByProjectId: (projectId) => { |
| 422 | if (threadId.length === 0) { | 465 | if (projectId.length === 0) { |
| 423 | return null; | 466 | return null; |
| 424 | } | 467 | } |
| 425 | return get().draftThreadsByThreadId[threadId] ?? null; | 468 | const threadId = get().projectDraftThreadIdByProjectId[projectId]; |
| 426 | }, | 469 | if (!threadId) { |
| 427 | isDraftThread: (threadId) => { | 470 | return null; |
| 428 | if (threadId.length === 0) { | | |
| 429 | return false; | | |
| 430 | } | 471 | } |
| 431 | return get().draftThreadsByThreadId[threadId] !== undefined; | 472 | const draftThread = get().draftThreadsByThreadId[threadId]; |
| | 473 | if (!draftThread || draftThread.projectId !== projectId) { |
| | 474 | return null; |
| | 475 | } |
| | 476 | return { |
| | 477 | threadId, |
| | 478 | ...draftThread, |
| | 479 | }; |
| 432 | }, | 480 | }, |
| 433 | readProjectDraftThreadId: (projectId) => { | 481 | getDraftThread: (threadId) => { |
| 434 | if (projectId.length === 0) { | 482 | if (threadId.length === 0) { |
| 435 | return null; | 483 | return null; |
| 436 | } | 484 | } |
| 437 | return get().projectDraftThreadIdByProjectId[projectId] ?? null; | 485 | return get().draftThreadsByThreadId[threadId] ?? null; |
| 438 | }, | 486 | }, |
| 439 | setProjectDraftThreadId: (projectId, threadId, options) => { | 487 | setProjectDraftThreadId: (projectId, threadId, options) => { |
| 440 | if (projectId.length === 0 || threadId.length === 0) { | 488 | if (projectId.length === 0 || threadId.length === 0) { |
| ... | @@ -502,7 +550,9 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -502,7 +550,9 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 502 | const nextDraftThread: DraftThreadState = { | 550 | const nextDraftThread: DraftThreadState = { |
| 503 | projectId: nextProjectId, | 551 | projectId: nextProjectId, |
| 504 | createdAt: | 552 | createdAt: |
| 505 | options.createdAt === undefined ? existing.createdAt : (options.createdAt || existing.createdAt), | 553 | options.createdAt === undefined |
| | 554 | ? existing.createdAt |
| | 555 | : options.createdAt || existing.createdAt, |
| 506 | branch: options.branch === undefined ? existing.branch : (options.branch ?? null), | 556 | branch: options.branch === undefined ? existing.branch : (options.branch ?? null), |
| 507 | worktreePath: | 557 | worktreePath: |
| 508 | options.worktreePath === undefined | 558 | options.worktreePath === undefined |
| ... | @@ -544,7 +594,8 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -544,7 +594,8 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 544 | if (threadId === undefined) { | 594 | if (threadId === undefined) { |
| 545 | return state; | 595 | return state; |
| 546 | } | 596 | } |
| 547 | const { [projectId]: _removed, ...restProjectMappingsRaw } = state.projectDraftThreadIdByProjectId; | 597 | const { [projectId]: _removed, ...restProjectMappingsRaw } = |
| | 598 | state.projectDraftThreadIdByProjectId; |
| 548 | const restProjectMappings = restProjectMappingsRaw as Record<ProjectId, ThreadId>; | 599 | const restProjectMappings = restProjectMappingsRaw as Record<ProjectId, ThreadId>; |
| 549 | const nextDraftThreadsByThreadId: Record<ThreadId, DraftThreadState> = { | 600 | const nextDraftThreadsByThreadId: Record<ThreadId, DraftThreadState> = { |
| 550 | ...state.draftThreadsByThreadId, | 601 | ...state.draftThreadsByThreadId, |
| ... | @@ -566,7 +617,8 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -566,7 +617,8 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 566 | if (state.projectDraftThreadIdByProjectId[projectId] !== threadId) { | 617 | if (state.projectDraftThreadIdByProjectId[projectId] !== threadId) { |
| 567 | return state; | 618 | return state; |
| 568 | } | 619 | } |
| 569 | const { [projectId]: _removed, ...restProjectMappingsRaw } = state.projectDraftThreadIdByProjectId; | 620 | const { [projectId]: _removed, ...restProjectMappingsRaw } = |
| | 621 | state.projectDraftThreadIdByProjectId; |
| 570 | const restProjectMappings = restProjectMappingsRaw as Record<ProjectId, ThreadId>; | 622 | const restProjectMappings = restProjectMappingsRaw as Record<ProjectId, ThreadId>; |
| 571 | const nextDraftThreadsByThreadId: Record<ThreadId, DraftThreadState> = { | 623 | const nextDraftThreadsByThreadId: Record<ThreadId, DraftThreadState> = { |
| 572 | ...state.draftThreadsByThreadId, | 624 | ...state.draftThreadsByThreadId, |
| ... | @@ -586,7 +638,9 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -586,7 +638,9 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 586 | } | 638 | } |
| 587 | set((state) => { | 639 | set((state) => { |
| 588 | const hasDraftThread = state.draftThreadsByThreadId[threadId] !== undefined; | 640 | const hasDraftThread = state.draftThreadsByThreadId[threadId] !== undefined; |
| 589 | const hasProjectMapping = Object.values(state.projectDraftThreadIdByProjectId).includes(threadId); | 641 | const hasProjectMapping = Object.values(state.projectDraftThreadIdByProjectId).includes( |
| | 642 | threadId, |
| | 643 | ); |
| 590 | if (!hasDraftThread && !hasProjectMapping) { | 644 | if (!hasDraftThread && !hasProjectMapping) { |
| 591 | return state; | 645 | return state; |
| 592 | } | 646 | } |
| ... | @@ -681,9 +735,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -681,9 +735,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 681 | return; | 735 | return; |
| 682 | } | 736 | } |
| 683 | const nextEffort = | 737 | const nextEffort = |
| 684 | effort && | 738 | effort && REASONING_EFFORT_VALUES.has(effort) && effort !== DEFAULT_REASONING |
| 685 | REASONING_EFFORT_VALUES.has(effort) && | | |
| 686 | effort !== DEFAULT_REASONING | | |
| 687 | ? effort | 739 | ? effort |
| 688 | : null; | 740 | : null; |
| 689 | set((state) => { | 741 | set((state) => { |
| ... | @@ -721,7 +773,9 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -721,7 +773,9 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 721 | set((state) => { | 773 | set((state) => { |
| 722 | const existing = state.draftsByThreadId[threadId] ?? createEmptyThreadDraft(); | 774 | const existing = state.draftsByThreadId[threadId] ?? createEmptyThreadDraft(); |
| 723 | const existingIds = new Set(existing.images.map((image) => image.id)); | 775 | const existingIds = new Set(existing.images.map((image) => image.id)); |
| 724 | const existingDedupKeys = new Set(existing.images.map((image) => composerImageDedupKey(image))); | 776 | const existingDedupKeys = new Set( |
| | 777 | existing.images.map((image) => composerImageDedupKey(image)), |
| | 778 | ); |
| 725 | const acceptedPreviewUrls = new Set(existing.images.map((image) => image.previewUrl)); | 779 | const acceptedPreviewUrls = new Set(existing.images.map((image) => image.previewUrl)); |
| 726 | const dedupedIncoming: ComposerImageAttachment[] = []; | 780 | const dedupedIncoming: ComposerImageAttachment[] = []; |
| 727 | for (const image of images) { | 781 | for (const image of images) { |
| ... | @@ -823,8 +877,8 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -823,8 +877,8 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 823 | ...current, | 877 | ...current, |
| 824 | // Stage attempted attachments so persist middleware can try writing them. | 878 | // Stage attempted attachments so persist middleware can try writing them. |
| 825 | persistedAttachments: attachments, | 879 | persistedAttachments: attachments, |
| 826 | nonPersistedImageIds: current.nonPersistedImageIds.filter((id) => | 880 | nonPersistedImageIds: current.nonPersistedImageIds.filter( |
| 827 | !attachmentIdSet.has(id), | 881 | (id) => !attachmentIdSet.has(id), |
| 828 | ), | 882 | ), |
| 829 | }; | 883 | }; |
| 830 | const nextDraftsByThreadId = { ...state.draftsByThreadId }; | 884 | const nextDraftsByThreadId = { ...state.draftsByThreadId }; |
| ... | @@ -836,7 +890,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -836,7 +890,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 836 | return { draftsByThreadId: nextDraftsByThreadId }; | 890 | return { draftsByThreadId: nextDraftsByThreadId }; |
| 837 | }); | 891 | }); |
| 838 | Promise.resolve().then(() => { | 892 | Promise.resolve().then(() => { |
| 839 | const persistedIdSet = new Set(readPersistedComposerDraftAttachmentIds(threadId)); | 893 | const persistedIdSet = new Set(readPersistedAttachmentIdsFromCache(threadId)); |
| 840 | set((state) => { | 894 | set((state) => { |
| 841 | const current = state.draftsByThreadId[threadId]; | 895 | const current = state.draftsByThreadId[threadId]; |
| 842 | if (!current) { | 896 | if (!current) { |
| ... | @@ -903,7 +957,9 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -903,7 +957,9 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 903 | set((state) => { | 957 | set((state) => { |
| 904 | const hasComposerDraft = state.draftsByThreadId[threadId] !== undefined; | 958 | const hasComposerDraft = state.draftsByThreadId[threadId] !== undefined; |
| 905 | const hasDraftThread = state.draftThreadsByThreadId[threadId] !== undefined; | 959 | const hasDraftThread = state.draftThreadsByThreadId[threadId] !== undefined; |
| 906 | const hasProjectMapping = Object.values(state.projectDraftThreadIdByProjectId).includes(threadId); | 960 | const hasProjectMapping = Object.values(state.projectDraftThreadIdByProjectId).includes( |
| | 961 | threadId, |
| | 962 | ); |
| 907 | if (!hasComposerDraft && !hasDraftThread && !hasProjectMapping) { | 963 | if (!hasComposerDraft && !hasDraftThread && !hasProjectMapping) { |
| 908 | return state; | 964 | return state; |
| 909 | } | 965 | } |
| ... | @@ -927,7 +983,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -927,7 +983,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 927 | { | 983 | { |
| 928 | name: COMPOSER_DRAFT_STORAGE_KEY, | 984 | name: COMPOSER_DRAFT_STORAGE_KEY, |
| 929 | version: 1, | 985 | version: 1, |
| 930 | storage: createJSONStorage(() => SAFE_LOCAL_STORAGE), | 986 | storage: composerDraftStorage, |
| 931 | partialize: (state) => { | 987 | partialize: (state) => { |
| 932 | const persistedDraftsByThreadId: PersistedComposerDraftStoreState["draftsByThreadId"] = {}; | 988 | const persistedDraftsByThreadId: PersistedComposerDraftStoreState["draftsByThreadId"] = {}; |
| 933 | for (const [threadId, draft] of Object.entries(state.draftsByThreadId)) { | 989 | for (const [threadId, draft] of Object.entries(state.draftsByThreadId)) { |
| ... | @@ -963,6 +1019,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -963,6 +1019,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 963 | }, | 1019 | }, |
| 964 | merge: (persistedState, currentState) => { | 1020 | merge: (persistedState, currentState) => { |
| 965 | const normalizedPersisted = normalizePersistedComposerDraftState(persistedState); | 1021 | const normalizedPersisted = normalizePersistedComposerDraftState(persistedState); |
| | 1022 | persistedDraftStateCache = normalizedPersisted; |
| 966 | const draftsByThreadId = Object.fromEntries( | 1023 | const draftsByThreadId = Object.fromEntries( |
| 967 | Object.entries(normalizedPersisted.draftsByThreadId).map(([threadId, draft]) => [ | 1024 | Object.entries(normalizedPersisted.draftsByThreadId).map(([threadId, draft]) => [ |
| 968 | threadId, | 1025 | threadId, |
| ... | @@ -984,19 +1041,6 @@ export function useComposerThreadDraft(threadId: ThreadId): ComposerThreadDraftS | ... | @@ -984,19 +1041,6 @@ export function useComposerThreadDraft(threadId: ThreadId): ComposerThreadDraftS |
| 984 | return useComposerDraftStore((state) => state.draftsByThreadId[threadId] ?? EMPTY_THREAD_DRAFT); | 1041 | return useComposerDraftStore((state) => state.draftsByThreadId[threadId] ?? EMPTY_THREAD_DRAFT); |
| 985 | } | 1042 | } |
| 986 | | 1043 | |
| 987 | export function readPersistedComposerDraftAttachmentIds(threadId: ThreadId): string[] { | 1044 | export function readComposerDraftPersistStorageChars(): number | null { |
| 988 | if (threadId.length === 0) { | 1045 | return persistedDraftStorageChars; |
| 989 | return []; | | |
| 990 | } | | |
| 991 | return readPersistedComposerDraftAttachments(threadId).map((attachment) => attachment.id); | | |
| 992 | } | | |
| 993 | | | |
| 994 | export function readPersistedComposerDraftAttachments( | | |
| 995 | threadId: ThreadId, | | |
| 996 | ): PersistedComposerImageAttachment[] { | | |
| 997 | if (threadId.length === 0) { | | |
| 998 | return []; | | |
| 999 | } | | |
| 1000 | const persisted = readPersistedDraftStateFromLocalStorage(); | | |
| 1001 | return persisted.draftsByThreadId[threadId]?.attachments ?? []; | | |
| 1002 | } | 1046 | } |