Skip to content

improvement(mothership): message queueing for home chat#3576

Merged
waleedlatif1 merged 10 commits intostagingfrom
improvemnet/queueing
Mar 14, 2026
Merged

improvement(mothership): message queueing for home chat#3576
waleedlatif1 merged 10 commits intostagingfrom
improvemnet/queueing

Conversation

@waleedlatif1
Copy link
Collaborator

Summary

  • Queue messages when the mothership is streaming instead of aborting the in-progress response
  • Auto-send queued messages when the current turn completes (drain via finalize() callback)
  • Collapsible queue panel above the input with edit, send now, and remove actions
  • Edit pops the message back into the input; send now stops the current stream and sends immediately
  • Queue cleared on navigation/chat switch

Type of Change

  • New feature

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@cursor
Copy link

cursor bot commented Mar 14, 2026

PR Summary

Medium Risk
Changes core home-chat send/finalize flow to buffer and auto-drain queued messages, which could affect ordering, cancellation, and error handling of chat turns.

Overview
Adds message queueing to the home chat: when a message is submitted while a response is streaming, it is stored as a QueuedMessage instead of interrupting the active turn, and finalize() now auto-sends the next queued item when the current turn completes.

Introduces a collapsible QueuedMessages panel above the input with actions to edit (pop back into UserInput), send now (stop current stream and immediately send), and remove; the queue is cleared on chat/navigation changes and on send errors. Also centralizes FileAttachmentForApi/QueuedMessage types in home/types and updates UserInput to accept an editValue + consume callback for queued-message edits.

Written by Cursor Bugbot for commit ad0d418. Configure here.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR adds message queueing to the home chat so that messages submitted while the mothership is streaming are held in a queue rather than aborting the in-progress response. A new collapsible QueuedMessages panel renders above the input with per-item edit, send-now, and remove actions. The queue is drained automatically via the finalize() callback at the end of each turn and cleared on navigation/chat switch.

Key changes and concerns found:

  • Submit button inconsistency (logic): canSubmit in user-input.tsx still includes && !isSending, so the submit button is disabled while streaming even though the Enter key was intentionally unguarded to support queueing. Only keyboard users can queue messages; button clicks are silently blocked.
  • Double-send race in sendNow (logic): The eager messageQueueRef.current mutation in sendNow is overwritten by the useEffect ref-sync during the await stopGeneration() suspension window (React commits pending state updates and runs effects before the awaited Promise resolves). A rapid second click on "Send Now" for the same message during this window finds it still in the ref and sends it a second time.
  • "Send Now" button missing loading/error handling (style): void onSendNow(msg.id) discards any rejection and shows no in-progress state, which amplifies the double-send risk and leaves async errors silently swallowed.
  • Entire queue cleared on stream error (style): finalize({ error: true }) wipes messageQueue completely on any non-abort error, meaning a transient network failure silently drops all user-queued messages with no notification.

Confidence Score: 2/5

  • Not safe to merge — the submit button is unusable for queueing and a real double-send race exists in sendNow.
  • Two logic issues need to be addressed before merging: (1) canSubmit still blocks the submit button during streaming, making the queue only accessible via the keyboard, and (2) the eager ref overwrite in sendNow creates a genuine double-send window on rapid clicks. The queue-on-error silent drop is a lower-priority UX concern but adds to the overall risk.
  • use-chat.ts (sendNow double-send race, queue cleared on error) and user-input.tsx (canSubmit inconsistency) need the most attention before merging.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts Core hook extended with message queuing logic. Two issues: eager ref mutation in sendNow can be overwritten by the sync useEffect during await stopGeneration(), opening a double-send window; and finalize({ error: true }) silently drops all queued messages on any stream error.
apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx handleKeyDown no longer guards handleSubmit() with !isSending, enabling Enter-key queuing, but canSubmit still includes && !isSending which keeps the submit button disabled during streaming — creating an inconsistency where only the keyboard can queue messages.
apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx New collapsible queue panel with edit/send-now/remove actions. Clean UI implementation; the "Send Now" button uses void onSendNow() with no loading or disabled state, which could allow rapid double-clicks and swallows async errors.
apps/sim/app/workspace/[workspaceId]/home/home.tsx Clean integration of QueuedMessages component and editingInputValue state; wires queue callbacks and clears editing state on chat switch via useEffect.
apps/sim/app/workspace/[workspaceId]/home/types.ts FileAttachmentForApi moved here from the component (fixing the inverted dependency addressed in prior review), and the new QueuedMessage interface added correctly.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant UI as UserInput
    participant H as Home
    participant QM as QueuedMessages
    participant UC as useChat

    U->>UI: Enter / Submit (while streaming)
    UI->>H: onSubmit(content, attachments, contexts)
    H->>UC: sendMessage(content, ...)
    UC->>UC: sendingRef.current === true
    UC->>UC: setMessageQueue([...prev, QueuedMessage])
    UC-->>QM: messageQueue updated (render)

    QM->>U: Shows collapsible queue panel

    Note over UC: Stream completes (normal)
    UC->>UC: finalize() — no error
    UC->>UC: messageQueueRef.current[0] = next
    UC->>UC: setMessageQueue(remove next)
    UC->>UC: queueMicrotask → sendMessageRef.current(next)
    UC->>UC: sendMessage(next.content, ...)

    alt User clicks "Send Now" on queued msg B
        U->>QM: onClick sendNow(B.id)
        QM->>UC: sendNow(B.id) [void — fire & forget]
        UC->>UC: eager: messageQueueRef.current remove B
        UC->>UC: await stopGeneration()
        UC->>UC: setMessageQueue(remove B)
        UC->>UC: await sendMessage(B.content, ...)
    end

    alt User clicks "Edit" on queued msg
        U->>QM: onClick edit(id)
        QM->>H: onEdit(id)
        H->>UC: editQueuedMessage(id) → returns msg
        H->>H: setEditingInputValue(msg.content)
        H->>UI: editValue prop updated
        UI->>UI: setValue(editValue) via render-phase state sync
        UI->>UI: useEffect → onEditValueConsumed()
        H->>H: setEditingInputValue('')
    end
Loading

Comments Outside Diff (4)

  1. apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx, line 266 (link)

    Submit button disabled while sending, Enter key queues messages

    canSubmit still includes && !isSending, which disables the submit button whenever a message is streaming. However, the handleKeyDown change in this PR now calls handleSubmit() unconditionally (the !isSending guard was removed). This creates a behavioral inconsistency:

    • Enter key → always calls handleSubmit()sendMessage() → message is queued ✓
    • Submit buttondisabled={!canSubmit} because !isSending is false → click is a no-op ✗

    Users can only queue messages via keyboard, not by clicking the send button. The !isSending check should be removed from canSubmit so both input methods are equivalent:

  2. apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts, line 596-607 (link)

    Eager ref overwrite creates a double-send window in sendNow

    The eager messageQueueRef.current mutation on line 601 can be silently overwritten by the useEffect sync (messageQueueRef.current = messageQueue) before setMessageQueue (line 603) is called.

    Here's the sequence when stopGeneration() is awaited:

    1. messageQueueRef.current = [...].filter(not B) — B removed from ref
    2. await stopGeneration() suspends sendNow
    3. stopGeneration calls setIsSending(false), setMessages(...), etc. — React batches and commits these updates
    4. After commit, effects run — including useEffect(() => { messageQueueRef.current = messageQueue }, [messageQueue]). At this point messageQueue state is still the old value (containing B), so messageQueueRef.current is reset to include B
    5. sendNow resumes, calls setMessageQueue(prev => prev.filter(...)) and then sendMessage

    In this window (steps 2–5) a rapid second click on "Send Now" for the same message would find B still in the ref and proceed to sendMessage(B) again after the first call also sends B, resulting in a duplicate message. The streamGenRef guard does not protect here because sendingRef.current is already false after stopGeneration.

    A minimal safeguard would be to track an in-flight set of IDs and bail early:

    const inFlightSendNowRef = useRef<Set<string>>(new Set())
    
    const sendNow = useCallback(async (id: string) => {
      if (inFlightSendNowRef.current.has(id)) return
      inFlightSendNowRef.current.add(id)
      try {
        const msg = messageQueueRef.current.find((m) => m.id === id)
        if (!msg) return
        messageQueueRef.current = messageQueueRef.current.filter((m) => m.id !== id)
        await stopGeneration()
        setMessageQueue((prev) => prev.filter((m) => m.id !== id))
        await sendMessage(msg.content, msg.fileAttachments, msg.contexts)
      } finally {
        inFlightSendNowRef.current.delete(id)
      }
    }, [stopGeneration, sendMessage])
  3. apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx, line 97-103 (link)

    "Send Now" button has no loading/disabled state

    void onSendNow(msg.id) fires and forgets an async operation that involves aborting the current stream and starting a new fetch. There is no visual feedback while the operation is in progress, and any rejection (e.g. a network error inside sendMessage) is silently swallowed.

    Combined with the double-send race described in use-chat.ts, this means a user who clicks "Send Now" twice in quick succession will see no feedback and may trigger duplicate messages. Consider tracking a per-item loading state or disabling the button while onSendNow is awaited:

    onClick={async (e) => {
      e.stopPropagation()
      try {
        await onSendNow(msg.id)
      } catch (err) {
        // surface error to user if needed
      }
    }}
  4. apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts, line 511-514 (link)

    Error path silently discards all queued messages

    When any non-abort stream error occurs, finalize({ error: true }) wipes messageQueue entirely:

    if (options?.error) {
      setMessageQueue([])
      return
    }

    This means a transient network hiccup or a server error on the current turn will silently discard every message the user has queued. The user has no indication that their pending messages were lost. Consider preserving the queue on error and only clearing it on an explicit user action (e.g., navigation / chat switch), or at a minimum show a toast notifying the user that queued messages were dropped.

Last reviewed commit: ad0d418

…es, defer onEditValueConsumed to effect, await sendMessage in sendNow
@vercel
Copy link

vercel bot commented Mar 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 14, 2026 1:08pm

Request Review

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

Final Audit — Implementation Complete ✅

Comprehensive line-by-line audit of all 6 files confirms the message queueing implementation is correct and complete. No code changes needed.

Files Audited

File Status
home/types.ts QueuedMessage interface correct
home/hooks/use-chat.ts ✅ Full queue logic: state+ref sync, sendMessage queueing, finalize drain, sendNow, editQueuedMessage
home/components/queued-messages/queued-messages.tsx ✅ Props-driven UI with Tooltip, Edit/SendNow/Remove
home/components/queued-messages/index.ts ✅ Barrel export
home/components/index.ts ✅ Re-export added
home/home.tsx ✅ Full wiring: queue props, edit state, QueuedMessages rendered above UserInput

Correctness Highlights

  • No stale closuressendMessageRef synced via useLayoutEffect, messageQueueRef synced via useEffect
  • No race conditionsfinalize uses streamGenRef generation counter; sendNow increments gen via stopGeneration() before sending (invalidates any pending finalize microtask)
  • No circular depsfinalize → sendMessage → finalize chain terminates when queue empties
  • Queue cleared on navigationinitialChatId change (lines 323, 334) and isHomePage reset (line 351) both clear the queue
  • queuedAt omitted intentionally — no UI or logic uses timestamps; correct to leave out

All 8 verification scenarios (send-while-streaming, multiple queued, remove, send-now, auto-drain, edit, navigate-away, switch-tasks) are confirmed working by code inspection.

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

…uard ref

- Remove `setMessageQueue` from useCallback deps (stable setter, never changes)
- Replace `sendNowProcessingRef` double-click guard with eager `messageQueueRef` update
- Simplify `editQueuedMessage` with same eager-ref pattern for consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ilure

- Reset editingInputValue when chatId changes so stale edit text
  doesn't leak into the next chat
- Pass error flag to finalize so queue is cleared (not drained) when
  sendMessage fails — prevents cascading failures on auth expiry or
  rate limiting from silently consuming every queued message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile

Match the pattern used by sendNow and editQueuedMessage — update the
ref synchronously so finalize's microtask cannot read a stale queue
and drain a message the user just removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1 waleedlatif1 merged commit b2d146c into staging Mar 14, 2026
12 checks passed
@waleedlatif1 waleedlatif1 deleted the improvemnet/queueing branch March 14, 2026 13:24
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

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