authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-01 11:54:23-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-01 22:36:35-08:00
log0b8dfee17beb957a8855f5c1382a110fc57de88a
tree4f066a866963be55b4988f18d27f8a03e1750dbb
parent6ab246676ebaee8ef895ab4067fb6457854b0a1c

Simplify composer draft persistence and remove debug instrumentation

- Remove ChatView draft persistence debug logging and storage size plumbing - Drop in-memory persisted draft cache; use JSON storage directly - Reconcile attachment IDs from localStorage and streamline image hydration

2 files changed, 26 insertions(+), 111 deletions(-)

apps/web/src/components/ChatView.tsx+2-42
......@@ -115,7 +115,6 @@ import {
115115 type ComposerImageAttachment,
116116 type DraftThreadState,
117117 type PersistedComposerImageAttachment,
118 readComposerDraftPersistStorageChars,
119118 useComposerDraftStore,
120119 useComposerThreadDraft,
121120} from "../composerDraftStore";
......@@ -138,8 +137,6 @@ const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120;
138137const SCRIPT_TERMINAL_COLS = 120;
139138const SCRIPT_TERMINAL_ROWS = 30;
140139const WORKTREE_BRANCH_PREFIX = "t3code";
141const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";
142const ENABLE_DRAFT_PERSIST_DEBUG_LOGS = import.meta.env.DEV;
143140
144141function readLastInvokedScriptByProjectFromStorage(): Record<string, string> {
145142 const stored = localStorage.getItem(LAST_INVOKED_SCRIPT_BY_PROJECT_KEY);
......@@ -294,13 +291,6 @@ function cloneComposerImageForRetry(image: ComposerImageAttachment): ComposerIma
294291 }
295292}
296293
297function debugDraftPersist(message: string, payload: Record<string, unknown>): void {
298 if (!ENABLE_DRAFT_PERSIST_DEBUG_LOGS) {
299 return;
300 }
301 console.debug(`[draft-persist] ${message}`, payload);
302}
303
304294const VscodeEntryIcon = memo(function VscodeEntryIcon(props: {
305295 pathValue: string;
306296 kind: "file" | "directory";
......@@ -1261,31 +1251,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
12611251 return;
12621252 }
12631253 // Stage attachments in persisted draft state first so persist middleware can write them.
1264 const stagedAttachmentIds = serialized.map((attachment) => attachment.id);
1265 debugDraftPersist("stage", {
1266 threadId,
1267 attachmentCount: serialized.length,
1268 serializationFailedCount: serializationFailedImageIds.length,
1269 serializationFailedImageIds,
1270 attachmentMeta: serialized.map((attachment) => ({
1271 id: attachment.id,
1272 name: attachment.name,
1273 sizeBytes: attachment.sizeBytes,
1274 dataUrlChars: attachment.dataUrl.length,
1275 })),
1276 storageCharsBefore: readComposerDraftPersistStorageChars(),
1277 });
1278 syncComposerDraftPersistedAttachments(
1279 threadId,
1280 serialized,
1281 );
1282 debugDraftPersist("reconcile-request", {
1283 threadId,
1284 stagedCount: stagedAttachmentIds.length,
1285 serializationFailedCount: serializationFailedImageIds.length,
1286 storageCharsAfter: readComposerDraftPersistStorageChars(),
1287 });
1288 } catch (error) {
1254 syncComposerDraftPersistedAttachments(threadId, serialized);
1255 } catch {
12891256 const currentImageIds = new Set(composerImages.map((image) => image.id));
12901257 const fallbackPersistedAttachments =
12911258 useComposerDraftStore.getState().draftsByThreadId[threadId]?.persistedAttachments ?? [];
......@@ -1296,13 +1263,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
12961263 const fallbackAttachments = fallbackPersistedAttachments.filter((attachment) =>
12971264 fallbackPersistedIdSet.has(attachment.id),
12981265 );
1299 debugDraftPersist("error", {
1300 threadId,
1301 error: error instanceof Error ? error.message : String(error),
1302 imageIds: composerImages.map((image) => image.id),
1303 fallbackPersistedCount: fallbackPersistedIds.length,
1304 storageChars: readComposerDraftPersistStorageChars(),
1305 });
13061266 if (cancelled) {
13071267 return;
13081268 }
apps/web/src/composerDraftStore.ts+24-69
......@@ -116,9 +116,6 @@ const EMPTY_PERSISTED_DRAFT_STORE_STATE: PersistedComposerDraftStoreState = {
116116 projectDraftThreadIdByProjectId: {},
117117};
118118
119let persistedDraftStateCache: PersistedComposerDraftStoreState = EMPTY_PERSISTED_DRAFT_STORE_STATE;
120let persistedDraftStorageChars: number | null = null;
121
122119const EMPTY_IMAGES: ComposerImageAttachment[] = [];
123120const EMPTY_IDS: string[] = [];
124121const EMPTY_PERSISTED_ATTACHMENTS: PersistedComposerImageAttachment[] = [];
......@@ -337,17 +334,25 @@ function parsePersistedDraftStateRaw(raw: string | null): PersistedComposerDraft
337334 }
338335}
339336
340function readPersistedAttachmentIdsFromCache(threadId: ThreadId): string[] {
337function readPersistedAttachmentIdsFromStorage(threadId: ThreadId): string[] {
341338 if (threadId.length === 0) {
342339 return [];
343340 }
344 return (persistedDraftStateCache.draftsByThreadId[threadId]?.attachments ?? []).map(
345 (attachment) => attachment.id,
346 );
341 try {
342 const raw = localStorage.getItem(COMPOSER_DRAFT_STORAGE_KEY);
343 const persisted = parsePersistedDraftStateRaw(raw);
344 return (persisted.draftsByThreadId[threadId]?.attachments ?? []).map(
345 (attachment) => attachment.id,
346 );
347 } catch {
348 return [];
349 }
347350}
348351
349function fileFromDataUrl(dataUrl: string, name: string, mimeType: string): File | null {
350 const [rawHeader, payload] = dataUrl.split(",", 2);
352function hydreatePersistedComposerImageAttachment(
353 attachment: PersistedComposerImageAttachment,
354): File | null {
355 const [rawHeader, payload] = attachment.dataUrl.split(",", 2);
351356 const header = rawHeader ?? "";
352357 if (!payload) {
353358 return null;
......@@ -359,15 +364,17 @@ function fileFromDataUrl(dataUrl: string, name: string, mimeType: string): File
359364 const inferredMimeType =
360365 header.startsWith("data:") && header.includes(";")
361366 ? header.slice("data:".length, header.indexOf(";"))
362 : mimeType;
363 return new File([decodedText], name, { type: inferredMimeType || mimeType });
367 : attachment.mimeType;
368 return new File([decodedText], attachment.name, {
369 type: inferredMimeType || attachment.mimeType,
370 });
364371 }
365372 const binary = atob(payload);
366373 const bytes = new Uint8Array(binary.length);
367374 for (let index = 0; index < binary.length; index += 1) {
368375 bytes[index] = binary.charCodeAt(index);
369376 }
370 return new File([bytes], name, { type: mimeType });
377 return new File([bytes], attachment.name, { type: attachment.mimeType });
371378 } catch {
372379 return null;
373380 }
......@@ -377,10 +384,9 @@ function hydrateImagesFromPersisted(
377384 attachments: PersistedComposerImageAttachment[],
378385): ComposerImageAttachment[] {
379386 return attachments.flatMap((attachment) => {
380 const file = fileFromDataUrl(attachment.dataUrl, attachment.name, attachment.mimeType);
381 if (!file) {
382 return [];
383 }
387 const file = hydreatePersistedComposerImageAttachment(attachment);
388 if (!file) return [];
389
384390 return [
385391 {
386392 type: "image" as const,
......@@ -409,52 +415,6 @@ function toHydratedThreadDraft(
409415 };
410416}
411417
412const 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
458418export const useComposerDraftStore = create<ComposerDraftStoreState>()(
459419 persist(
460420 (set, get) => ({
......@@ -890,7 +850,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
890850 return { draftsByThreadId: nextDraftsByThreadId };
891851 });
892852 Promise.resolve().then(() => {
893 const persistedIdSet = new Set(readPersistedAttachmentIdsFromCache(threadId));
853 const persistedIdSet = new Set(readPersistedAttachmentIdsFromStorage(threadId));
894854 set((state) => {
895855 const current = state.draftsByThreadId[threadId];
896856 if (!current) {
......@@ -983,7 +943,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
983943 {
984944 name: COMPOSER_DRAFT_STORAGE_KEY,
985945 version: 1,
986 storage: composerDraftStorage,
946 storage: createJSONStorage(() => localStorage),
987947 partialize: (state) => {
988948 const persistedDraftsByThreadId: PersistedComposerDraftStoreState["draftsByThreadId"] = {};
989949 for (const [threadId, draft] of Object.entries(state.draftsByThreadId)) {
......@@ -1019,7 +979,6 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
1019979 },
1020980 merge: (persistedState, currentState) => {
1021981 const normalizedPersisted = normalizePersistedComposerDraftState(persistedState);
1022 persistedDraftStateCache = normalizedPersisted;
1023982 const draftsByThreadId = Object.fromEntries(
1024983 Object.entries(normalizedPersisted.draftsByThreadId).map(([threadId, draft]) => [
1025984 threadId,
......@@ -1040,7 +999,3 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
1040999export function useComposerThreadDraft(threadId: ThreadId): ComposerThreadDraftState {
10411000 return useComposerDraftStore((state) => state.draftsByThreadId[threadId] ?? EMPTY_THREAD_DRAFT);
10421001}
1043
1044export function readComposerDraftPersistStorageChars(): number | null {
1045 return persistedDraftStorageChars;
1046}