mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 14:58:39 +08:00
* feat(chat): queue pending user messages with sending/error/retry states Replace the single optimisticUserMessage slot with a FIFO queue of pending user messages so the user gets immediate feedback when they hit send and multiple in-flight messages do not clobber each other. - Reshape optimistic-user-message-store around enqueuePendingMessage / consumeOldestSendingMessage / markPendingMessageError / markPendingMessageSending with a 'sending' | 'error' status per entry. - Render queued messages via a new PendingUserMessages component using ChatMessage's new pendingStatus + onRetry props, applying a faded treatment for 'sending' and an error banner + retry link for 'error'. - Wire the WebSocket context to consume the oldest 'sending' entry when the server echoes a UserMessageEvent back, so the queue drains FIFO. - Update ChatInterface, GitControlBar, TaskCard and useHandleBuildPlanClick to enqueue pending messages, and flip the matching entry to 'error' when the send call rejects. - Add i18n strings for Sending / Send failed / Retry. Co-authored-by: openhands <openhands@all-hands.dev> * fix(chat): scope pending message queue per conversation and stop double 'Sending' render Two follow-up fixes on top of the pending-message-queue refactor: 1. **Per-conversation scoping.** The optimistic queue is global but each entry is now tagged with the `conversationId` it was enqueued from, and `PendingUserMessages` filters to entries matching the active conversation via `useOptionalConversationId`. This means switching conversations no longer carries 'Sending…' bubbles over, and the WebSocket `UserMessageEvent` ack for one conversation can never pop a pending entry belonging to another. `consumeOldestSendingMessage` now takes the conversation id and only matches within it. Call sites updated: `chat-interface.tsx`, `git-control-bar.tsx`, `use-handle-build-plan-click.ts`. `task-card.tsx` moves the enqueue into the `createConversation` `onSuccess` callback so the message is tagged with the newly-created conversation id. 2. **Duplicate 'Sending…' bubble.** `<PendingUserMessages />` was rendered from two places after the previous rebase: once at the bottom of `<Messages>` (where the pending queue would never re-render anyway because `Messages` is `React.memo`'d on event ids) and once unconditionally from `<ChatInterface>`. Removed the render inside `<Messages>` so only the ChatInterface render remains. Also fixed a separate double-submit path in `custom-chat-input.tsx`: the `submittedMessage` effect listed `onSubmit` in its deps, but `onSubmit` (= `handleSendMessage`) is a fresh function on every parent render, and the parent re-renders synchronously when the new `enqueuePendingMessage` call mutates the store. That made the effect fire twice for the same `submittedMessage` value before `setSubmittedMessage(null)` flushed. Pinned `onSubmit` behind a ref and dropped it from the dep array. Tests updated to pass `conversationId` and a new test asserts the queue is not consumed across conversations. Co-authored-by: openhands <openhands@all-hands.dev> * fix(chat): address review feedback on pending-message queue - plan-preview.test.tsx: include `useOptionalConversationId` in the `#/hooks/use-conversation-id` mock so the test passes now that `useHandleBuildPlanClick` depends on it. This was failing all 21 PlanPreview tests in CI on the previous push. - optimistic-user-message-store: replace the module-level counter (which never reset between tests) with a `Date.now()` + base36 random suffix. Same uniqueness guarantee, no shared state. - optimistic-user-message-store: collapse `consumeOldestSendingMessage` into a single atomic `set()` so the find and the filter can't observe an interleaved update from another action. - git-control-bar: capture the `pendingId` from the `enqueuePendingMessage` return value and, if the `send` promise rejects, mark that entry as error so the user gets a retry link instead of a stuck 'Sending…' bubble. - chat-message: add `role="status" aria-live="polite"` to the 'Sending…' label and `role="alert"` to the error label so screen readers announce send-state transitions. Co-authored-by: openhands <openhands@all-hands.dev> * fix(chat): content-keyed echo matching + 60s watchdog timeout for pending messages Addresses the bot's reposted 'critical' review threads on PR #281. **Out-of-order echo matching.** - `PendingUserMessage` now stores both `text` (user-visible bubble) and `content` (the exact string sent to the server, which may include the appended 'Files uploaded: …' prompt). `enqueuePendingMessage` accepts an optional `content` and defaults it to `text` for call sites that don't transform the prompt. - New `consumeMatchingPendingMessage(conversationId, content)`. It does an exact content match first — so an echo of 'second' arriving before an echo of 'hello' correctly pops 'second' instead of the oldest entry — and falls back to the oldest "sending" entry in the same conversation if no exact match exists (lets us still drain the queue if the server slightly munges the body). - `conversation-websocket-context` extracts the echoed text by joining the `TextContent` parts of `event.llm_message.content` and passes it to the new matcher. Both consumption sites (main WS + planning agent WS) use the matcher with the main `conversationId`, so a planning sub-agent echo can never consume a main-conversation pending entry. - `chat-interface` passes `content: prompt` (text + file annotations) to `enqueuePendingMessage`. **60-second watchdog timeout.** - `enqueuePendingMessage` now schedules a `setTimeout` for `PENDING_MESSAGE_TIMEOUT_MS` (60s, exported). If the entry is still in 'sending' state when the timer fires, it's flipped to 'error' with message 'Send timed out' so the user gets a retry link instead of a permanently-stuck bubble. Timeout is a no-op if the echo already consumed the message or if it was already marked error explicitly (the original `errorMessage` is preserved). - 60s is long enough to cover legitimately slow uploads / agent-server latency but short enough to actually rescue stuck bubbles. **Tests.** - `optimistic-user-message-store.test.ts` adds coverage for: storing separate `text`/`content`, exact-match preference, FIFO fallback, skipping entries already in 'error', cross-conversation isolation with identical content, watchdog timeout firing, and the two no-op cases (echo already consumed, message already failed). Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev>
Test Helpers
This directory contains reusable test utilities and components for the OpenHands frontend test suite.
Files
websocket-test-components.tsx
Contains React test components for accessing and displaying WebSocket-related store values:
ConnectionStatusComponent- Displays WebSocket connection stateEventStoreComponent- Displays event store values (events count, UI events count, latest event ID)OptimisticUserMessageStoreComponent- Displays optimistic user message store valuesErrorMessageStoreComponent- Displays error message store values
These components are designed to be used in tests to verify that WebSocket events are properly processed and stored.
msw-websocket-setup.ts
Contains MSW (Mock Service Worker) setup utilities for WebSocket testing:
createWebSocketLink()- Creates a WebSocket link for MSW testingcreateWebSocketMockServer()- Creates and configures an MSW server for WebSocket testingcreateWebSocketTestSetup()- Creates a complete WebSocket testing setupconversationWebSocketTestSetup()- Standard setup for conversation WebSocket handler tests
Usage
import {
ConnectionStatusComponent,
EventStoreComponent,
} from "./__tests__/helpers/websocket-test-components";
import { conversationWebSocketTestSetup } from "./__tests__/helpers/msw-websocket-setup";
// Set up MSW server
const { wsLink, server } = conversationWebSocketTestSetup();
// Render components with WebSocket context (helper function defined in test file)
renderWithWebSocketContext(<ConnectionStatusComponent />);
Benefits
- Reusability: Test components and utilities can be shared across multiple test files
- Maintainability: Changes to test setup only need to be made in one place
- Consistency: Ensures consistent test setup across different WebSocket-related tests
- Readability: Test files are cleaner and focus on test logic rather than setup boilerplate