Skip to content

Conversation

@ThomasK33
Copy link
Member

Persist pasted image attachments in ChatInput drafts so they survive workspace switches and app restarts.

Key points:

  • Added inputImages:<scopeId> localStorage key and included it in workspace storage copy/delete logic.
  • ChatInput now restores persisted image attachments on mount and persists updates on paste/drop/remove/send.
  • Creation flow clears pending image drafts after successful workspace creation.

Tests:

  • make static-check

📋 Implementation Plan

Persist pasted image attachments in ChatInput drafts

Why images are currently lost

  • Draft text is persisted via usePersistedState(getInputKey(...)) in src/browser/components/ChatInput/index.tsx.
  • Draft images are held only in component memory (useState<ImageAttachment[]>([])), so they reset whenever:
    • the workspace view remounts (workspace switch), or
    • the renderer reloads (app restart).

Goal

Make pasted/drag-dropped image attachments persist the same way draft text does:

  • per-workspace (and per “creation” project scope)
  • survives workspace switches and app restarts
  • doesn’t regress the ability to attach large images (quota issues should not break the UI)

Recommended approach (minimal + safe)

Persist image attachments best-effort to localStorage under a new key, while keeping the authoritative in-memory state in React.

Rationale:

  • Matches existing draft persistence strategy (localStorage) without introducing new backend APIs.
  • Avoids a regression where attaching a large image fails to show in the UI if localStorage quota is exceeded (which would happen if we used usePersistedState as the source of truth).

Implementation plan

1) Add a workspace-scoped storage key for draft images

Files:

  • src/common/constants/storage.ts

Actions:

  • Add a new helper:
    • getInputImagesKey(scopeId: string): stringinputImages:${scopeId}
    • (Use the same scopeId inputs as getInputKey: workspaceId for workspace variant; getPendingScopeId(projectPath) for creation variant.)
  • Add getInputImagesKey to PERSISTENT_WORKSPACE_KEY_FUNCTIONS so images are:
    • copied on workspace fork (copyWorkspaceStorage)
    • deleted on workspace removal (deleteWorkspaceStorage)
    • migrated on workspace ID migration (migrateWorkspaceStorage)

2) Load persisted images when ChatInput mounts

Files:

  • src/browser/components/ChatInput/index.tsx

Actions:

  • Extend storageKeys to include imagesKey:
    • workspace: getInputImagesKey(props.workspaceId)
    • creation: getInputImagesKey(getPendingScopeId(props.projectPath))
  • Initialize imageAttachments from localStorage:
    • useState(() => readPersistedState<ImageAttachment[]>(storageKeys.imagesKey, []))
  • Add a small runtime validator (defensive programming):
    • if the persisted value isn’t an array of {id,url,mediaType} strings, log and fall back to [] (and consider clearing the bad key).

3) Persist image changes back to localStorage (best-effort)

Files:

  • src/browser/components/ChatInput/index.tsx

Actions:

  • Keep imageAttachments as a normal useState.
  • Wrap updates so they also persist:
    • When setting images, call updatePersistedState(storageKeys.imagesKey, nextImages.length ? nextImages : undefined).
    • Ensure all mutation paths persist:
      • paste (handlePaste)
      • drop (handleDrop)
      • remove (handleRemoveImage)
      • clear on send success
      • restoreImages API
      • edit-mode transitions via setDraft

Optional but recommended (quota UX):

  • Add a simple size guard before persisting:
    • compute JSON.stringify(nextImages).length
    • if above a conservative threshold (e.g. 6–8MB chars), skip persistence and show a toast like:
      • “Image draft too large to save; it will be lost on restart.”
    • Still keep imageAttachments in memory so the user can send the message.

4) Clear pending image drafts on successful workspace creation

Files:

  • src/browser/components/ChatInput/useCreationWorkspace.ts

Actions:

  • When creation succeeds (where we already clear pendingInputKey), also clear:
    • updatePersistedState(getInputImagesKey(getPendingScopeId(projectPath)), undefined)

5) Tests

Goal: prevent regressions in the future.

Recommended tests:

  • src/common/constants/storage.test.ts
    • verifies getInputImagesKey() format
    • verifies copyWorkspaceStorage() copies the image key (using a fake localStorage)
    • verifies deleteWorkspaceStorage() removes the image key
  • src/browser/components/ChatInput test (new):
    • pre-populate localStorage with inputImages:<workspaceId> containing one ImageAttachment
    • render ChatInput for that workspace and assert the thumbnail <img src=...> is present
    • update state (e.g., remove image) and assert localStorage key is removed/updated

6) Manual QA checklist

  • Workspace draft persistence:
    1. Open workspace A → paste an image + type text.
    2. Switch to workspace B → switch back to A.
    3. Confirm text + image thumbnail are still present.
  • Restart persistence:
    1. With an image attached in workspace A draft, fully restart Mux.
    2. Confirm the draft image is restored.
  • Creation flow:
    1. Go to creation mode → paste an image.
    2. Create workspace.
    3. Confirm the pending draft is cleared (image doesn’t “stick around” in future creation attempts).
  • Fork/removal:
    • Fork a workspace with a draft image: image draft should copy.
    • Delete a workspace: image draft key should be removed.

Alternative: store draft images on disk (more robust, more work)

If localStorage size limits or sync write jank are a concern, store draft images under ~/.mux/sessions/<workspaceId>/draft-images.json using SessionFileManager (server-side) and expose workspace.getDraft() / workspace.setDraft() ORPC endpoints.

Pros:

  • avoids localStorage quota
  • avoids large synchronous localStorage writes in the renderer

Cons:

  • new backend API surface
  • more wiring + tests (IPC/orpc + session file persistence)

Generated with mux • Model: openai:gpt-5.2 • Thinking: xhigh

@chatgpt-codex-connector
Copy link

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

Change-Id: I8855c5220767d93c110f83faf01857d0aa03d762
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Ic7dd775a044d3f35cf3c5fbba3429ab173b916d0
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I958dee73468007cd6376ddf03ad77f34b60611b0
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33 ThomasK33 force-pushed the save-images-draft-message branch from d5bb9ef to c66fbc2 Compare December 16, 2025 11:34
@ThomasK33 ThomasK33 added this pull request to the merge queue Dec 16, 2025
Merged via the queue into main with commit e1be6b4 Dec 16, 2025
20 checks passed
@ThomasK33 ThomasK33 deleted the save-images-draft-message branch December 16, 2025 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant