| author | |
| committer | |
| log | b59c6d6ab1b71295bf8c77e611522989e1e97fc8 |
| tree | 6c9af7fdfede05ab08cccb21817347be5965a661 |
| parent | c9995995a42317676290ca1b953dfeabdedd8920 |
- move project draft thread ID persistence/actions into `composerDraftStore`
- remove legacy `projectDraftThreads` module and update `ChatView`/`Sidebar` callers
- keep image dedupe stable across hydration and avoid revoking blob URLs on composer clear
- add store tests for project-thread mapping and composer clear behavior
- drop deprecated model slug aliases (`gpt-5`, `gpt-5-codex`, `gpt-5-codex-spark`)6 files changed, 180 insertions(+), 123 deletions(-)
apps/web/src/components/ChatView.tsx+3-1| ... | ... | @@ -118,7 +118,6 @@ import { |
| 118 | 118 | useComposerDraftStore, |
| 119 | 119 | useComposerThreadDraft, |
| 120 | 120 | } from "../composerDraftStore"; |
| 121 | import { clearProjectDraftThreadById } from "../projectDraftThreads"; | |
| 122 | 121 | import { clamp } from "effect/Number"; |
| 123 | 122 | |
| 124 | 123 | function formatMessageMeta(createdAt: string, duration: string | null): string { |
| ... | ... | @@ -429,6 +428,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 429 | 428 | (store) => store.syncPersistedAttachments, |
| 430 | 429 | ); |
| 431 | 430 | const clearComposerDraftContent = useComposerDraftStore((store) => store.clearComposerContent); |
| 431 | const clearProjectDraftThreadById = useComposerDraftStore( | |
| 432 | (store) => store.clearProjectDraftThreadById, | |
| 433 | ); | |
| 432 | 434 | const promptRef = useRef(prompt); |
| 433 | 435 | const [isDragOverComposer, setIsDragOverComposer] = useState(false); |
| 434 | 436 | const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); |
apps/web/src/components/Sidebar.tsx+21-11| ... | ... | @@ -21,12 +21,6 @@ import { gitRemoveWorktreeMutationOptions, gitStatusQueryOptions } from "../lib/ |
| 21 | 21 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; |
| 22 | 22 | import { readNativeApi } from "../nativeApi"; |
| 23 | 23 | import { useComposerDraftStore } from "../composerDraftStore"; |
| 24 | import { | |
| 25 | clearProjectDraftThreadById, | |
| 26 | clearProjectDraftThreadId, | |
| 27 | readProjectDraftThreadId, | |
| 28 | writeProjectDraftThreadId, | |
| 29 | } from "../projectDraftThreads"; | |
| 30 | 24 | import { toastManager } from "./ui/toast"; |
| 31 | 25 | import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible"; |
| 32 | 26 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| ... | ... | @@ -240,6 +234,12 @@ function ProjectFavicon({ cwd }: { cwd: string }) { |
| 240 | 234 | export default function Sidebar() { |
| 241 | 235 | const { state, dispatch } = useStore(); |
| 242 | 236 | const clearComposerDraftForThread = useComposerDraftStore((store) => store.clearThreadDraft); |
| 237 | const readProjectDraftThreadId = useComposerDraftStore((store) => store.readProjectDraftThreadId); | |
| 238 | const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId); | |
| 239 | const clearProjectDraftThreadId = useComposerDraftStore((store) => store.clearProjectDraftThreadId); | |
| 240 | const clearProjectDraftThreadById = useComposerDraftStore( | |
| 241 | (store) => store.clearProjectDraftThreadById, | |
| 242 | ); | |
| 243 | 243 | const navigate = useNavigate(); |
| 244 | 244 | const { settings: appSettings } = useAppSettings(); |
| 245 | 245 | const routeThreadId = useParams({ |
| ... | ... | @@ -388,7 +388,7 @@ export default function Sidebar() { |
| 388 | 388 | if (reusableStoredDraftThread) { |
| 389 | 389 | return (async () => { |
| 390 | 390 | await applyThreadOptions(reusableStoredDraftThread); |
| 391 | writeProjectDraftThreadId(projectId, reusableStoredDraftThread.id); | |
| 391 | setProjectDraftThreadId(projectId, reusableStoredDraftThread.id); | |
| 392 | 392 | if (routeThreadId === reusableStoredDraftThread.id) { |
| 393 | 393 | return; |
| 394 | 394 | } |
| ... | ... | @@ -414,7 +414,7 @@ export default function Sidebar() { |
| 414 | 414 | if (reusableDraftThread) { |
| 415 | 415 | return (async () => { |
| 416 | 416 | await applyThreadOptions(reusableDraftThread); |
| 417 | writeProjectDraftThreadId(projectId, reusableDraftThread.id); | |
| 417 | setProjectDraftThreadId(projectId, reusableDraftThread.id); | |
| 418 | 418 | if (routeThreadId === reusableDraftThread.id) { |
| 419 | 419 | return; |
| 420 | 420 | } |
| ... | ... | @@ -440,7 +440,7 @@ export default function Sidebar() { |
| 440 | 440 | worktreePath: options?.worktreePath ?? null, |
| 441 | 441 | createdAt, |
| 442 | 442 | }); |
| 443 | writeProjectDraftThreadId(projectId, threadId); | |
| 443 | setProjectDraftThreadId(projectId, threadId); | |
| 444 | 444 | |
| 445 | 445 | // Ensure route guards can see the new thread before navigating. |
| 446 | 446 | try { |
| ... | ... | @@ -456,7 +456,16 @@ export default function Sidebar() { |
| 456 | 456 | }); |
| 457 | 457 | })(); |
| 458 | 458 | }, |
| 459 | [dispatch, navigate, routeThreadId, state.projects, state.threads], | |
| 459 | [ | |
| 460 | clearProjectDraftThreadId, | |
| 461 | dispatch, | |
| 462 | navigate, | |
| 463 | readProjectDraftThreadId, | |
| 464 | routeThreadId, | |
| 465 | setProjectDraftThreadId, | |
| 466 | state.projects, | |
| 467 | state.threads, | |
| 468 | ], | |
| 460 | 469 | ); |
| 461 | 470 | |
| 462 | 471 | const focusMostRecentThreadForProject = useCallback( |
| ... | ... | @@ -648,6 +657,7 @@ export default function Sidebar() { |
| 648 | 657 | [ |
| 649 | 658 | appSettings.confirmThreadDelete, |
| 650 | 659 | clearComposerDraftForThread, |
| 660 | clearProjectDraftThreadById, | |
| 651 | 661 | dispatch, |
| 652 | 662 | navigate, |
| 653 | 663 | removeWorktreeMutation, |
| ... | ... | @@ -709,7 +719,7 @@ export default function Sidebar() { |
| 709 | 719 | }); |
| 710 | 720 | } |
| 711 | 721 | }, |
| 712 | [clearComposerDraftForThread, state.projects, state.threads], | |
| 722 | [clearComposerDraftForThread, clearProjectDraftThreadId, state.projects, state.threads], | |
| 713 | 723 | ); |
| 714 | 724 | |
| 715 | 725 | useEffect(() => { |
apps/web/src/composerDraftStore.test.ts+70-3| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | import { ThreadId } from "@t3tools/contracts"; | |
| 1 | import { ProjectId, ThreadId } from "@t3tools/contracts"; | |
| 2 | 2 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 3 | 3 | |
| 4 | 4 | import { type ComposerImageAttachment, useComposerDraftStore } from "./composerDraftStore"; |
| ... | ... | @@ -33,7 +33,7 @@ describe("composerDraftStore addImages", () => { |
| 33 | 33 | let revokeSpy: ReturnType<typeof vi.fn<(url: string) => void>>; |
| 34 | 34 | |
| 35 | 35 | beforeEach(() => { |
| 36 | useComposerDraftStore.setState({ draftsByThreadId: {} }); | |
| 36 | useComposerDraftStore.setState({ draftsByThreadId: {}, projectDraftThreadIdByProjectId: {} }); | |
| 37 | 37 | originalRevokeObjectUrl = URL.revokeObjectURL; |
| 38 | 38 | revokeSpy = vi.fn(); |
| 39 | 39 | URL.revokeObjectURL = revokeSpy; |
| ... | ... | @@ -83,7 +83,7 @@ describe("composerDraftStore addImages", () => { |
| 83 | 83 | name: "same.png", |
| 84 | 84 | mimeType: "image/png", |
| 85 | 85 | sizeBytes: 9, |
| 86 | lastModified: 777, | |
| 86 | lastModified: 999, | |
| 87 | 87 | }); |
| 88 | 88 | |
| 89 | 89 | useComposerDraftStore.getState().addImage(threadId, first); |
| ... | ... | @@ -111,3 +111,70 @@ describe("composerDraftStore addImages", () => { |
| 111 | 111 | expect(revokeSpy).not.toHaveBeenCalledWith("blob:shared"); |
| 112 | 112 | }); |
| 113 | 113 | }); |
| 114 | ||
| 115 | describe("composerDraftStore clearComposerContent", () => { | |
| 116 | const threadId = ThreadId.makeUnsafe("thread-clear"); | |
| 117 | let originalRevokeObjectUrl: typeof URL.revokeObjectURL; | |
| 118 | let revokeSpy: ReturnType<typeof vi.fn<(url: string) => void>>; | |
| 119 | ||
| 120 | beforeEach(() => { | |
| 121 | useComposerDraftStore.setState({ draftsByThreadId: {}, projectDraftThreadIdByProjectId: {} }); | |
| 122 | originalRevokeObjectUrl = URL.revokeObjectURL; | |
| 123 | revokeSpy = vi.fn(); | |
| 124 | URL.revokeObjectURL = revokeSpy; | |
| 125 | }); | |
| 126 | ||
| 127 | afterEach(() => { | |
| 128 | URL.revokeObjectURL = originalRevokeObjectUrl; | |
| 129 | }); | |
| 130 | ||
| 131 | it("does not revoke blob preview URLs when clearing composer content", () => { | |
| 132 | const first = makeImage({ | |
| 133 | id: "img-optimistic", | |
| 134 | previewUrl: "blob:optimistic", | |
| 135 | }); | |
| 136 | useComposerDraftStore.getState().addImage(threadId, first); | |
| 137 | ||
| 138 | useComposerDraftStore.getState().clearComposerContent(threadId); | |
| 139 | ||
| 140 | const draft = useComposerDraftStore.getState().draftsByThreadId[threadId]; | |
| 141 | expect(draft).toBeUndefined(); | |
| 142 | expect(revokeSpy).not.toHaveBeenCalledWith("blob:optimistic"); | |
| 143 | }); | |
| 144 | }); | |
| 145 | ||
| 146 | describe("composerDraftStore project draft thread mapping", () => { | |
| 147 | const projectId = ProjectId.makeUnsafe("project-a"); | |
| 148 | const threadId = ThreadId.makeUnsafe("thread-a"); | |
| 149 | const otherThreadId = ThreadId.makeUnsafe("thread-b"); | |
| 150 | ||
| 151 | beforeEach(() => { | |
| 152 | useComposerDraftStore.setState({ draftsByThreadId: {}, projectDraftThreadIdByProjectId: {} }); | |
| 153 | }); | |
| 154 | ||
| 155 | it("stores and reads project draft thread ids via actions", () => { | |
| 156 | const store = useComposerDraftStore.getState(); | |
| 157 | expect(store.readProjectDraftThreadId(projectId)).toBeNull(); | |
| 158 | ||
| 159 | store.setProjectDraftThreadId(projectId, threadId); | |
| 160 | expect(useComposerDraftStore.getState().readProjectDraftThreadId(projectId)).toBe(threadId); | |
| 161 | }); | |
| 162 | ||
| 163 | it("clears only matching project draft mapping entries", () => { | |
| 164 | const store = useComposerDraftStore.getState(); | |
| 165 | store.setProjectDraftThreadId(projectId, threadId); | |
| 166 | ||
| 167 | store.clearProjectDraftThreadById(projectId, otherThreadId); | |
| 168 | expect(useComposerDraftStore.getState().readProjectDraftThreadId(projectId)).toBe(threadId); | |
| 169 | ||
| 170 | store.clearProjectDraftThreadById(projectId, threadId); | |
| 171 | expect(useComposerDraftStore.getState().readProjectDraftThreadId(projectId)).toBeNull(); | |
| 172 | }); | |
| 173 | ||
| 174 | it("clears project draft mapping by project id", () => { | |
| 175 | const store = useComposerDraftStore.getState(); | |
| 176 | store.setProjectDraftThreadId(projectId, threadId); | |
| 177 | store.clearProjectDraftThreadId(projectId); | |
| 178 | expect(useComposerDraftStore.getState().readProjectDraftThreadId(projectId)).toBeNull(); | |
| 179 | }); | |
| 180 | }); |
apps/web/src/composerDraftStore.ts+86-14| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import { |
| 2 | 2 | DEFAULT_REASONING, |
| 3 | ProjectId, | |
| 3 | 4 | REASONING_OPTIONS, |
| 4 | 5 | ThreadId, |
| 5 | 6 | normalizeModelSlug, |
| ... | ... | @@ -67,6 +68,7 @@ interface PersistedComposerThreadDraftState { |
| 67 | 68 | |
| 68 | 69 | interface PersistedComposerDraftStoreState { |
| 69 | 70 | draftsByThreadId: Record<ThreadId, PersistedComposerThreadDraftState>; |
| 71 | projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>; | |
| 70 | 72 | } |
| 71 | 73 | |
| 72 | 74 | export interface ComposerThreadDraftState { |
| ... | ... | @@ -81,6 +83,11 @@ export interface ComposerThreadDraftState { |
| 81 | 83 | |
| 82 | 84 | interface ComposerDraftStoreState { |
| 83 | 85 | draftsByThreadId: Record<ThreadId, ComposerThreadDraftState>; |
| 86 | projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>; | |
| 87 | readProjectDraftThreadId: (projectId: ProjectId) => ThreadId | null; | |
| 88 | setProjectDraftThreadId: (projectId: ProjectId, threadId: ThreadId) => void; | |
| 89 | clearProjectDraftThreadId: (projectId: ProjectId) => void; | |
| 90 | clearProjectDraftThreadById: (projectId: ProjectId, threadId: ThreadId) => void; | |
| 84 | 91 | setPrompt: (threadId: ThreadId, prompt: string) => void; |
| 85 | 92 | setCursor: (threadId: ThreadId, cursor: number) => void; |
| 86 | 93 | setModel: (threadId: ThreadId, model: string | null | undefined) => void; |
| ... | ... | @@ -122,8 +129,9 @@ function createEmptyThreadDraft(): ComposerThreadDraftState { |
| 122 | 129 | } |
| 123 | 130 | |
| 124 | 131 | function composerImageDedupKey(image: ComposerImageAttachment): string { |
| 125 | const lastModified = Number.isFinite(image.file.lastModified) ? image.file.lastModified : 0; | |
| 126 | return `${image.mimeType}\u0000${image.sizeBytes}\u0000${lastModified}\u0000${image.name}`; | |
| 132 | // Keep this independent from File.lastModified so dedupe is stable for hydrated | |
| 133 | // images reconstructed from localStorage (which get a fresh lastModified value). | |
| 134 | return `${image.mimeType}\u0000${image.sizeBytes}\u0000${image.name}`; | |
| 127 | 135 | } |
| 128 | 136 | |
| 129 | 137 | function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { |
| ... | ... | @@ -179,12 +187,29 @@ function normalizePersistedAttachment(value: unknown): PersistedComposerImageAtt |
| 179 | 187 | |
| 180 | 188 | function normalizePersistedComposerDraftState(value: unknown): PersistedComposerDraftStoreState { |
| 181 | 189 | if (!value || typeof value !== "object") { |
| 182 | return { draftsByThreadId: {} }; | |
| 190 | return { draftsByThreadId: {}, projectDraftThreadIdByProjectId: {} }; | |
| 183 | 191 | } |
| 184 | 192 | const candidate = value as Record<string, unknown>; |
| 185 | 193 | const rawDraftMap = candidate.draftsByThreadId; |
| 194 | const rawProjectDraftThreadIdByProjectId = candidate.projectDraftThreadIdByProjectId; | |
| 195 | const projectDraftThreadIdByProjectId: PersistedComposerDraftStoreState["projectDraftThreadIdByProjectId"] = | |
| 196 | {}; | |
| 197 | if (rawProjectDraftThreadIdByProjectId && typeof rawProjectDraftThreadIdByProjectId === "object") { | |
| 198 | for (const [projectId, threadId] of Object.entries( | |
| 199 | rawProjectDraftThreadIdByProjectId as Record<string, unknown>, | |
| 200 | )) { | |
| 201 | if ( | |
| 202 | typeof projectId === "string" && | |
| 203 | projectId.length > 0 && | |
| 204 | typeof threadId === "string" && | |
| 205 | threadId.length > 0 | |
| 206 | ) { | |
| 207 | projectDraftThreadIdByProjectId[projectId as ProjectId] = threadId as ThreadId; | |
| 208 | } | |
| 209 | } | |
| 210 | } | |
| 186 | 211 | if (!rawDraftMap || typeof rawDraftMap !== "object") { |
| 187 | return { draftsByThreadId: {} }; | |
| 212 | return { draftsByThreadId: {}, projectDraftThreadIdByProjectId }; | |
| 188 | 213 | } |
| 189 | 214 | const nextDraftsByThreadId: PersistedComposerDraftStoreState["draftsByThreadId"] = {}; |
| 190 | 215 | for (const [threadId, draftValue] of Object.entries(rawDraftMap as Record<string, unknown>)) { |
| ... | ... | @@ -225,7 +250,7 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer |
| 225 | 250 | ...(effort ? { effort } : {}), |
| 226 | 251 | }; |
| 227 | 252 | } |
| 228 | return { draftsByThreadId: nextDraftsByThreadId }; | |
| 253 | return { draftsByThreadId: nextDraftsByThreadId, projectDraftThreadIdByProjectId }; | |
| 229 | 254 | } |
| 230 | 255 | |
| 231 | 256 | function fileFromDataUrl(dataUrl: string, name: string, mimeType: string): File | null { |
| ... | ... | @@ -279,12 +304,12 @@ function hydrateImagesFromPersisted( |
| 279 | 304 | |
| 280 | 305 | function readPersistedDraftStateFromLocalStorage(): PersistedComposerDraftStoreState { |
| 281 | 306 | if (typeof window === "undefined") { |
| 282 | return { draftsByThreadId: {} }; | |
| 307 | return { draftsByThreadId: {}, projectDraftThreadIdByProjectId: {} }; | |
| 283 | 308 | } |
| 284 | 309 | try { |
| 285 | 310 | const raw = window.localStorage.getItem(COMPOSER_DRAFT_STORAGE_KEY); |
| 286 | 311 | if (!raw) { |
| 287 | return { draftsByThreadId: {} }; | |
| 312 | return { draftsByThreadId: {}, projectDraftThreadIdByProjectId: {} }; | |
| 288 | 313 | } |
| 289 | 314 | const parsed = JSON.parse(raw) as unknown; |
| 290 | 315 | if (parsed && typeof parsed === "object" && "state" in parsed) { |
| ... | ... | @@ -292,7 +317,7 @@ function readPersistedDraftStateFromLocalStorage(): PersistedComposerDraftStoreS |
| 292 | 317 | } |
| 293 | 318 | return normalizePersistedComposerDraftState(parsed); |
| 294 | 319 | } catch { |
| 295 | return { draftsByThreadId: {} }; | |
| 320 | return { draftsByThreadId: {}, projectDraftThreadIdByProjectId: {} }; | |
| 296 | 321 | } |
| 297 | 322 | } |
| 298 | 323 | |
| ... | ... | @@ -314,6 +339,57 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 314 | 339 | persist( |
| 315 | 340 | (set, get) => ({ |
| 316 | 341 | draftsByThreadId: {}, |
| 342 | projectDraftThreadIdByProjectId: {}, | |
| 343 | readProjectDraftThreadId: (projectId) => { | |
| 344 | if (projectId.length === 0) { | |
| 345 | return null; | |
| 346 | } | |
| 347 | return get().projectDraftThreadIdByProjectId[projectId] ?? null; | |
| 348 | }, | |
| 349 | setProjectDraftThreadId: (projectId, threadId) => { | |
| 350 | if (projectId.length === 0 || threadId.length === 0) { | |
| 351 | return; | |
| 352 | } | |
| 353 | set((state) => { | |
| 354 | if (state.projectDraftThreadIdByProjectId[projectId] === threadId) { | |
| 355 | return state; | |
| 356 | } | |
| 357 | return { | |
| 358 | projectDraftThreadIdByProjectId: { | |
| 359 | ...state.projectDraftThreadIdByProjectId, | |
| 360 | [projectId]: threadId, | |
| 361 | }, | |
| 362 | }; | |
| 363 | }); | |
| 364 | }, | |
| 365 | clearProjectDraftThreadId: (projectId) => { | |
| 366 | if (projectId.length === 0) { | |
| 367 | return; | |
| 368 | } | |
| 369 | set((state) => { | |
| 370 | if (state.projectDraftThreadIdByProjectId[projectId] === undefined) { | |
| 371 | return state; | |
| 372 | } | |
| 373 | const { [projectId]: _removed, ...rest } = state.projectDraftThreadIdByProjectId; | |
| 374 | return { | |
| 375 | projectDraftThreadIdByProjectId: rest, | |
| 376 | }; | |
| 377 | }); | |
| 378 | }, | |
| 379 | clearProjectDraftThreadById: (projectId, threadId) => { | |
| 380 | if (projectId.length === 0 || threadId.length === 0) { | |
| 381 | return; | |
| 382 | } | |
| 383 | set((state) => { | |
| 384 | if (state.projectDraftThreadIdByProjectId[projectId] !== threadId) { | |
| 385 | return state; | |
| 386 | } | |
| 387 | const { [projectId]: _removed, ...rest } = state.projectDraftThreadIdByProjectId; | |
| 388 | return { | |
| 389 | projectDraftThreadIdByProjectId: rest, | |
| 390 | }; | |
| 391 | }); | |
| 392 | }, | |
| 317 | 393 | setPrompt: (threadId, prompt) => { |
| 318 | 394 | if (threadId.length === 0) { |
| 319 | 395 | return; |
| ... | ... | @@ -579,12 +655,6 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 579 | 655 | if (threadId.length === 0) { |
| 580 | 656 | return; |
| 581 | 657 | } |
| 582 | const existing = get().draftsByThreadId[threadId]; | |
| 583 | if (existing) { | |
| 584 | for (const image of existing.images) { | |
| 585 | revokeObjectPreviewUrl(image.previewUrl); | |
| 586 | } | |
| 587 | } | |
| 588 | 658 | set((state) => { |
| 589 | 659 | const current = state.draftsByThreadId[threadId]; |
| 590 | 660 | if (!current) { |
| ... | ... | @@ -659,6 +729,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 659 | 729 | } |
| 660 | 730 | return { |
| 661 | 731 | draftsByThreadId: persistedDraftsByThreadId, |
| 732 | projectDraftThreadIdByProjectId: state.projectDraftThreadIdByProjectId, | |
| 662 | 733 | }; |
| 663 | 734 | }, |
| 664 | 735 | merge: (persistedState, currentState) => { |
| ... | ... | @@ -672,6 +743,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 672 | 743 | return { |
| 673 | 744 | ...currentState, |
| 674 | 745 | draftsByThreadId, |
| 746 | projectDraftThreadIdByProjectId: normalizedPersisted.projectDraftThreadIdByProjectId, | |
| 675 | 747 | }; |
| 676 | 748 | }, |
| 677 | 749 | }, |
apps/web/src/projectDraftThreads.ts deleted-91| ... | ... | @@ -1,91 +0,0 @@ |
| 1 | import { ProjectId, ThreadId } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | const PROJECT_DRAFT_THREADS_STORAGE_KEY = "t3code:project-draft-threads:v1"; | |
| 4 | ||
| 5 | type ProjectDraftThreadMap = Record<ProjectId, ThreadId>; | |
| 6 | ||
| 7 | function readAllProjectDraftThreads(): ProjectDraftThreadMap { | |
| 8 | if (typeof window === "undefined") { | |
| 9 | return {}; | |
| 10 | } | |
| 11 | try { | |
| 12 | const raw = window.localStorage.getItem(PROJECT_DRAFT_THREADS_STORAGE_KEY); | |
| 13 | if (!raw) { | |
| 14 | return {}; | |
| 15 | } | |
| 16 | const parsed = JSON.parse(raw) as unknown; | |
| 17 | if (!parsed || typeof parsed !== "object") { | |
| 18 | return {}; | |
| 19 | } | |
| 20 | const next: ProjectDraftThreadMap = {}; | |
| 21 | for (const [projectId, threadId] of Object.entries(parsed as Record<string, unknown>)) { | |
| 22 | if (typeof projectId !== "string" || typeof threadId !== "string") { | |
| 23 | continue; | |
| 24 | } | |
| 25 | if (projectId.length === 0 || threadId.length === 0) { | |
| 26 | continue; | |
| 27 | } | |
| 28 | next[projectId as ProjectId] = threadId as ThreadId; | |
| 29 | } | |
| 30 | return next; | |
| 31 | } catch { | |
| 32 | return {}; | |
| 33 | } | |
| 34 | } | |
| 35 | ||
| 36 | function persistAllProjectDraftThreads(next: ProjectDraftThreadMap): void { | |
| 37 | if (typeof window === "undefined") { | |
| 38 | return; | |
| 39 | } | |
| 40 | try { | |
| 41 | const entries = Object.entries(next); | |
| 42 | if (entries.length === 0) { | |
| 43 | window.localStorage.removeItem(PROJECT_DRAFT_THREADS_STORAGE_KEY); | |
| 44 | return; | |
| 45 | } | |
| 46 | window.localStorage.setItem(PROJECT_DRAFT_THREADS_STORAGE_KEY, JSON.stringify(next)); | |
| 47 | } catch { | |
| 48 | // Best-effort persistence only. | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | export function readProjectDraftThreadId(projectId: ProjectId): ThreadId | null { | |
| 53 | if (projectId.length === 0) { | |
| 54 | return null; | |
| 55 | } | |
| 56 | const all = readAllProjectDraftThreads(); | |
| 57 | return all[projectId] ?? null; | |
| 58 | } | |
| 59 | ||
| 60 | export function writeProjectDraftThreadId(projectId: ProjectId, threadId: ThreadId): void { | |
| 61 | if (projectId.length === 0 || threadId.length === 0) { | |
| 62 | return; | |
| 63 | } | |
| 64 | const all = readAllProjectDraftThreads(); | |
| 65 | all[projectId] = threadId; | |
| 66 | persistAllProjectDraftThreads(all); | |
| 67 | } | |
| 68 | ||
| 69 | export function clearProjectDraftThreadId(projectId: ProjectId): void { | |
| 70 | if (projectId.length === 0) { | |
| 71 | return; | |
| 72 | } | |
| 73 | const all = readAllProjectDraftThreads(); | |
| 74 | if (all[projectId] === undefined) { | |
| 75 | return; | |
| 76 | } | |
| 77 | const { [projectId]: _removed, ...rest } = all; | |
| 78 | persistAllProjectDraftThreads(rest); | |
| 79 | } | |
| 80 | ||
| 81 | export function clearProjectDraftThreadById(projectId: ProjectId, threadId: ThreadId): void { | |
| 82 | if (projectId.length === 0 || threadId.length === 0) { | |
| 83 | return; | |
| 84 | } | |
| 85 | const all = readAllProjectDraftThreads(); | |
| 86 | if (all[projectId] !== threadId) { | |
| 87 | return; | |
| 88 | } | |
| 89 | const { [projectId]: _removed, ...rest } = all; | |
| 90 | persistAllProjectDraftThreads(rest); | |
| 91 | } |
packages/contracts/src/model.ts-3| ... | ... | @@ -11,11 +11,8 @@ export type ModelSlug = string; |
| 11 | 11 | export const DEFAULT_MODEL = "gpt-5.3-codex"; |
| 12 | 12 | |
| 13 | 13 | export const MODEL_SLUG_ALIASES: Record<string, BuiltInModelSlug> = { |
| 14 | "gpt-5": "gpt-5.3-codex", | |
| 15 | "gpt-5-codex": "gpt-5.3-codex", | |
| 16 | 14 | "5.3": "gpt-5.3-codex", |
| 17 | 15 | "gpt-5.3": "gpt-5.3-codex", |
| 18 | "gpt-5-codex-spark": "gpt-5.3-codex-spark", | |
| 19 | 16 | "5.3-spark": "gpt-5.3-codex-spark", |
| 20 | 17 | "gpt-5.3-spark": "gpt-5.3-codex-spark", |
| 21 | 18 | }; |