fix(chat): allow dismissing failed pending user messages (#16142)

Co-authored-by: syf2211 <syf2211@users.noreply.github.com>
This commit is contained in:
石岳峰
2026-07-31 08:08:09 -04:00
committed by GitHub
co-authored by syf2211
parent 2feab551c4
commit 2be11e861b
5 changed files with 87 additions and 0 deletions
@@ -209,6 +209,27 @@ describe("ChatMessage", () => {
expect(onStop).toHaveBeenCalledTimes(1);
});
it("shows retry and dismiss controls for a failed user message", async () => {
const onRetry = vi.fn();
const onDismiss = vi.fn();
render(
<ChatMessage
type="user"
message="Failed to deliver"
pendingStatus="error"
onRetry={onRetry}
onDismiss={onDismiss}
/>,
);
expect(screen.getByTestId("chat-message-error")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("chat-message-retry"));
fireEvent.click(screen.getByTestId("chat-message-dismiss"));
expect(onRetry).toHaveBeenCalledTimes(1);
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it("renders a literal angle-bracket user message as visible text", () => {
// Regression: user messages were rendered with raw-HTML parsing enabled,
// so a message like "<something>" was parsed as an unknown HTML tag and
@@ -132,6 +132,31 @@ describe("PendingUserMessages", () => {
expect(message).toHaveAttribute("data-pending-status", "error");
expect(screen.getByTestId("chat-message-error")).toBeInTheDocument();
expect(screen.getByTestId("chat-message-retry")).toBeInTheDocument();
expect(screen.getByTestId("chat-message-dismiss")).toBeInTheDocument();
});
it("removes a failed message when dismiss is clicked", async () => {
const id = useOptimisticUserMessageStore
.getState()
.enqueuePendingMessage({
conversationId: ACTIVE_CONVO,
text: "dismiss me",
});
useOptimisticUserMessageStore
.getState()
.markPendingMessageError(id, "Server unavailable");
renderWithProviders(<PendingUserMessages />);
const user = userEvent.setup();
await user.click(screen.getByTestId("chat-message-dismiss"));
await waitFor(() => {
expect(screen.queryByTestId("user-message")).not.toBeInTheDocument();
});
expect(useOptimisticUserMessageStore.getState().pendingMessages).toHaveLength(
0,
);
});
it("re-sends and flips back to 'sending' when retry is clicked", async () => {
@@ -27,6 +27,7 @@ interface ChatMessageProps {
isFromPlanningAgent?: boolean;
pendingStatus?: ChatMessagePendingStatus;
onRetry?: () => void;
onDismiss?: () => void;
onStop?: () => void;
}
@@ -38,6 +39,7 @@ export function ChatMessage({
isFromPlanningAgent = false,
pendingStatus,
onRetry,
onDismiss,
onStop,
}: React.PropsWithChildren<ChatMessageProps>) {
const { t } = useTranslation("openhands");
@@ -256,6 +258,16 @@ export function ChatMessage({
{t(I18nKey.CHAT_INTERFACE$MESSAGE_RETRY)}
</button>
) : null}
{onDismiss ? (
<button
type="button"
onClick={onDismiss}
className="cursor-pointer rounded-md border border-[var(--oh-border)] px-2 py-1 text-xs font-normal text-[var(--oh-foreground)] hover:bg-[var(--oh-interactive-hover)]"
data-testid="chat-message-dismiss"
>
{t(I18nKey.CHAT_INTERFACE$MESSAGE_DISMISS)}
</button>
) : null}
</div>
</div>
);
@@ -92,6 +92,13 @@ export function PendingUserMessages() {
[restoreMessageToInputIfEmpty, removePendingMessage],
);
const handleDismiss = React.useCallback(
(id: string) => {
removePendingMessage(id);
},
[removePendingMessage],
);
if (visibleMessages.length === 0) {
return null;
}
@@ -109,6 +116,11 @@ export function PendingUserMessages() {
? () => handleRetry(message.id)
: undefined
}
onDismiss={
message.status === "error"
? () => handleDismiss(message.id)
: undefined
}
onStop={
message.status === "sending"
? () => handleStop(message.id, message.text)
+17
View File
@@ -9859,6 +9859,23 @@
"uk": "Повторити",
"ca": "Torna-ho a provar"
},
"CHAT_INTERFACE$MESSAGE_DISMISS": {
"en": "Dismiss",
"ja": "閉じる",
"zh-CN": "关闭",
"zh-TW": "關閉",
"ko-KR": "닫기",
"no": "Lukk",
"it": "Chiudi",
"pt": "Dispensar",
"es": "Descartar",
"ar": "إغلاق",
"fr": "Fermer",
"tr": "Kapat",
"de": "Schließen",
"uk": "Закрити",
"ca": "Tanca"
},
"CHAT_INTERFACE$AUGMENTED_PROMPT_FILES_TITLE": {
"en": "NEW FILES ADDED",
"de": "NEUE DATEIEN HINZUGEFÜGT",