Skip to content

feat(chat): attachment UX, upload-as-file, and home/cloud submit fixes#712

Open
FraterCCCLXIII wants to merge 19 commits into
OpenHands:mainfrom
FraterCCCLXIII:extra-tools-dropdown
Open

feat(chat): attachment UX, upload-as-file, and home/cloud submit fixes#712
FraterCCCLXIII wants to merge 19 commits into
OpenHands:mainfrom
FraterCCCLXIII:extra-tools-dropdown

Conversation

@FraterCCCLXIII
Copy link
Copy Markdown
Contributor

Summary

This PR improves chat attachment UX and fixes several home-to-conversation launch bugs, especially on cloud backends.

Chat input & attachments

  • Merge the tools menu into the + button and add an Add Files and Images footer action with a paperclip icon.
  • Support clipboard image paste on home and conversation inputs.
  • Add a per-image upload-as-file toggle on attachment thumbnails (all attached images, not just pastes). Images default to inline base64 embed; toggling uploads them into the conversation workspace instead.
  • Replace the global upload-as-file checkbox with a compact FilePlus toggle on each image preview (checkmark when active).
  • Upload attachments into the conversation working_dir (fixes read-only /workspace paths in Docker dev stacks).

Home & cloud conversation start

  • Defer home attachments until cloud start tasks finish provisioning (task-{uuid} → real conversation id).
  • Route cloud home uploads and first messages through the runtime sandbox (avoids 404s against the bundled local agent-server).
  • Send home attachments in a single first message (no duplicate text-only + attachments sends).
  • Show the user's home submit as an optimistic pending message during cloud provisioning.
  • Link pending messages across task-to-conversation redirect so they survive navigation.

Chat feed polish (new conversations)

  • Suppress spurious "failed to load old messages" errors on fresh conversations and during provisioning.
  • Hide the history skeleton when a pending user message is already visible.
  • Stop "Let's Start Building!" suggestions from flashing after home submit on cloud.

Test plan

  • Home: paste an image, toggle upload-as-file, submit — one message with expected inline vs workspace behavior
  • Home: attach files + text — single first message, no duplicates
  • Conversation input: paste/drag images — FilePlus toggle visible on all image thumbnails
  • Local backend: start conversation with attachments — uploads land in conversation workspace
  • Cloud backend: start conversation with attachments — no 422 on task-{uuid}, attachments flush after READY
  • Cloud backend: home submit shows user message immediately, no suggestions/skeleton flash
  • Existing conversation: scroll-up pagination still works; no error banner on brand-new chats
  • npm test passes

Made with Cursor

FraterCCCLXIII and others added 18 commits May 20, 2026 10:30
Combine the chat tools dropdown with the + control and add an
Add Files and Images action with a paperclip icon at the bottom.

Co-authored-by: Cursor <cursoragent@cursor.com>
…puts

Read pasted screenshots from clipboard items, wire the home launcher
through the shared attachment upload flow, and send first messages with
attachments after creating a conversation.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the global checkbox with a circular upload button on clipboard
pasted images, track paste source in the store, and fix overlay stacking.

Co-authored-by: Cursor <cursoragent@cursor.com>
Match the remove button size and corner inset, show a checkmark when
active with hover feedback, and swap the tooltip to "Do not upload as file".

Co-authored-by: Cursor <cursoragent@cursor.com>
Match the chat input container's top inset so pasted images and files
are spaced evenly above and below the thumbnail strip.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace invalid primary tokens with oh-surface/oh-muted styling for both
states and raise the button slightly from the thumbnail corner.

Co-authored-by: Cursor <cursoragent@cursor.com>
Cloud conversation creation can return a provisional `task-{uuid}` URL
while the sandbox provisions. Sending messages or uploading files against
that id caused 422 UUID parsing errors on the home `/conversations` input.

- Queue attachments in memory keyed by start-task id when provisioning
- Flush uploads and the user message once `useTaskPolling` sees READY
- Always include typed text in the start request, even with attachments
- Add store and flush helper tests

Paste/drag on the home input were already wired; this fixes starting a
conversation with attachments (or text + attachments) on cloud backends.

Co-authored-by: Cursor <cursoragent@cursor.com>
File uploads targeted read-only /workspace in Docker dev stacks and
cloud task flush used raw i18next before app init. Resolve the
conversation working_dir for upload paths and use the initialized
openhands i18n instance when flushing deferred attachments.

Co-authored-by: Cursor <cursoragent@cursor.com>
Cloud file uploads and the first message were hitting the bundled
local agent-server and 404ing. Defer home attachments until the start
task is ready, upload via the provisioned runtime URL, and send events
through the cloud proxy.

Co-authored-by: Cursor <cursoragent@cursor.com>
Skip initial_message when starting with attachments and avoid enqueueing
optimistic duplicates after the attachment send already persisted the message.

Co-authored-by: Cursor <cursoragent@cursor.com>
Enqueue optimistic pending messages on cloud start-task routes so the chat
shows the user's message while the sandbox provisions, hide empty-state
suggestions during provisioning, and reassign pending bubbles to the real
conversation id when the task is ready.

Co-authored-by: Cursor <cursoragent@cursor.com>
Skip auto-pagination when the initial history page is complete, on cloud
start-task routes, or while provisioning, and stop surfacing an error
banner when older events cannot be anchored.

Co-authored-by: Cursor <cursoragent@cursor.com>
Treat optimistic home-submit bubbles as loaded content so the feed
skeleton does not flash over the user's first message on new conversations.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep pending bubbles linked across task-to-conversation redirect, reassign
them before paint on the real route, and tighten suggestion gating so the
Let's Start Building overlay does not flash during provisioning.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mark every attached image for the per-image upload control, not just
clipboard pastes, so file picker and drag-and-drop previews match pasted
screenshot behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the generic upload glyph with Lucide FilePlus so the per-image
control reads more clearly as "add this image as a workspace file."

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

@FraterCCCLXIII is attempting to deploy a commit to the openhands Team on Vercel.

A member of the Team first needs to authorize it.

@FraterCCCLXIII
Copy link
Copy Markdown
Contributor Author

Images here:

image image image image

Copy link
Copy Markdown
Contributor

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR significantly improves the attachment UX and fixes several cloud conversation start issues. However, there are critical race conditions in the new task/attachment coordination code that must be addressed before merging.

🔴 Critical Issues:

  1. pending-task-message-link.ts line 29: Single scheduledPendingReassign variable will be overwritten by concurrent task completions
  2. use-task-polling.ts lines 106-124: Async IIFE has no cancellation - will navigate after unmount
  3. pending-task-attachments-store.ts lines 30-36: Non-atomic consume operation allows double-consumption
  4. home-chat-launcher.tsx lines 59-62: imagesMarkedUploadAsFile not snapshotted - causes array index mismatches

🟠 Important Issues:
5. enqueue-home-task-pending-message.ts line 19: No error handling for image conversion failures
6. send-message-with-attachments.ts line 49: Promise.all fails completely if one image fails (should use Promise.allSettled)
7. use-load-older-events.ts line 156: setState after unmount (missing cancellation ref)
8. chat-interface.tsx lines 312, 327: Non-null assertions without guards
9. home-chat-launcher.tsx line 112: Passing null to displayErrorToast (poor UX)

🟡 Code quality:
10. conversation-file-upload.api.ts line 134: Redundant getSafeUploadFileName call
11. send-message-with-attachments.ts line 64: Empty content creates malformed prompt with leading newlines

See inline comments for the critical race conditions. The Important and Code quality issues should also be addressed but are less severe.


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/agent-canvas/actions/runs/26241015556

fromConversationId: string,
toConversationId: string,
): void {
scheduledPendingReassign = { fromConversationId, toConversationId };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical - Race Condition: The single scheduledPendingReassign variable will be overwritten if multiple cloud tasks complete concurrently. If a user starts two conversations from the home page rapidly, the second call to schedulePendingTaskMessageReassign() overwrites the first, causing the first task's pending messages to never be reassigned.

Fix: Replace with a Map keyed by toConversationId to handle multiple concurrent tasks.

})),

consumePendingTaskAttachments: (taskId) => {
const payload = get().byTaskId[taskId];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical - Race Condition: Non-atomic consume operation. The get() on line 31 and set() on line 36 are separate operations. If consumePendingTaskAttachments() is called twice rapidly with the same taskId, both calls could read the same payload before either removes it, violating consume-once semantics.

Fix: Make the read-and-remove atomic by doing both inside a single setState() callback, similar to consumeMatchingPendingMessage in optimistic-user-message-store.ts.

Copy link
Copy Markdown
Contributor

@hieptl hieptl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @FraterCCCLXIII,

I identified a few issues while testing the pull request locally.

Issue 1 (Blocking):

Step 1: Connect to the OpenHands Cloud Backend (https://app.all-hands.dev).
Step 2: Enter and send a message using the chat input on the home page.
Step 3: The sent message appears duplicated and remains in a pending state even after the agent has responded.

Please refer to the video below for additional context.

issue1.mov

Issue 2 (Blocking):

Step 1: Connect to the OpenHands Cloud Backend (https://app.all-hands.dev).
Step 2: Attach and send an image.
Step 3: The agent does not appear to recognize the attached image.

Please refer to the video below for additional context.

issue2.mov

Thank you very much! 🙏

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.

3 participants