Add message queue to chat UI#36
Conversation
Greptile SummaryThis PR adds a message queue to the chat UI, allowing users to keep composing and queueing follow-up messages while a response is still streaming. Serialization is keyed off the
Confidence Score: 5/5Safe to merge — the queue serialization logic is sound, the cleanup correctly handles both same-route and cross-route navigation, and the previously noted leakage on New Chat navigation is fixed by this PR. The core serialization relies on the No files require special attention. The two minor observations are about the Important Files Changed
Sequence DiagramsequenceDiagram
participant U as User
participant CI as ChatInput
participant CP as ChatPage
participant MQ as MessageQueue (singleton)
participant UC as useChat.sendMessage
U->>CI: "types & clicks Send (idle)"
CI->>CP: handleSendMessage(content, files)
CP->>MQ: sendOrQueue(content, files)
Note over MQ: busy=false → pump() starts
MQ->>MQ: setBusy(true)
MQ->>UC: send(content, files) [await]
Note over CI: isQueuing=true → button shows Queue
U->>CI: "types & clicks Queue (busy)"
CI->>CP: handleSendMessage(content2, files2)
CP->>MQ: sendOrQueue(content2, files2)
Note over MQ: busy=true → append to queue, emit chip
UC-->>MQ: Promise resolves (turn complete)
MQ->>MQ: dequeue next message
MQ->>UC: send(content2, files2) [await]
UC-->>MQ: Promise resolves
MQ->>MQ: queue empty → setBusy(false)
Note over CI: isQueuing=false → button shows Send
U->>CP: navigate to new conversation
CP->>MQ: clearQueue() [via useEffect cleanup]
Note over MQ: queued messages dropped, busy unchanged
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
ui/src/pages/chat/useMessageQueue.ts:15-17
External store subscription with `useState` + `useEffect` has a small tearing window: `isBusy` (and `queuedMessages`) are snapshotted at render time via `useState(chatMessageQueue.isBusy)`, but the subscription that keeps them up to date is wired only after commit. If the singleton's busy state transitions between render and the effect running, that transition is missed. The canonical React solution for subscribing to an external mutable store is `useSyncExternalStore`, which eliminates the gap and also handles concurrent-mode tearing.
```suggestion
export function useMessageQueue(send: SendFn) {
const queuedMessages = useSyncExternalStore(
(cb) => chatMessageQueue.subscribe((_q) => cb()),
() => chatMessageQueue.getQueue(),
() => chatMessageQueue.getQueue()
);
const isBusy = useSyncExternalStore(
(cb) => chatMessageQueue.subscribeBusy((_b) => cb()),
() => chatMessageQueue.isBusy,
() => chatMessageQueue.isBusy
);
```
### Issue 2 of 2
ui/src/components/ChatInput/ChatInput.tsx:795-797
When `isStreaming` is true and `isQueuing` is false (an edit-and-rerun/regenerate stream), the primary button is correctly disabled, but its aria-label and visible text both read "Queue" — telling a screen-reader user they can queue a message when they actually cannot. The label should reflect the true state of the control.
```suggestion
aria-label={isQueuing ? "Queue message" : "Send message"}
>
{isQueuing ? "Queue" : "Send"}
```
Reviews (7): Last reviewed commit: "Review fixes" | Re-trigger Greptile |
No description provided.