fix(frontend): dismissible & expandable error banner (#12354)

Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
This commit is contained in:
Vedant Madane
2026-01-16 02:48:37 +05:30
committed by GitHub
parent 53f86955e0
commit efb54fd791
7 changed files with 212 additions and 30 deletions

View File

@@ -20,7 +20,7 @@ This is the frontend of the OpenHands project. It is a React application that pr
### Prerequisites
- Node.js 20.x or later
- Node.js 22.12.x or later
- `npm`, `bun`, or any other package manager that supports the `package.json` file
### Installation

View File

@@ -0,0 +1,34 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { ErrorMessageBanner } from "#/components/features/chat/error-message-banner";
describe("ErrorMessageBanner", () => {
it("calls onDismiss when the close button is clicked", async () => {
const user = userEvent.setup();
const onDismiss = vi.fn();
render(
<ErrorMessageBanner
message="Something went wrong"
onDismiss={onDismiss}
/>,
);
await user.click(screen.getByLabelText("BUTTON$CLOSE"));
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it("shows a View More / View Less toggle for long messages", async () => {
const user = userEvent.setup();
const longMessage = "a".repeat(400);
render(<ErrorMessageBanner message={longMessage} />);
const toggle = screen.getByTestId("error-message-banner-toggle");
expect(toggle).toHaveTextContent("COMMON$VIEW_MORE");
await user.click(toggle);
expect(toggle).toHaveTextContent("COMMON$VIEW_LESS");
});
});

View File

@@ -89,7 +89,7 @@
"vitest": "^4.0.14"
},
"engines": {
"node": ">=22.0.0"
"node": ">=22.12.0"
}
},
"node_modules/@acemir/cssom": {
@@ -192,6 +192,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -731,6 +732,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -777,6 +779,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -2345,6 +2348,7 @@
"version": "2.4.25",
"resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.25.tgz",
"integrity": "sha512-F6UUoGTQ+Qas5wYkCzLjXE7u74Z9ygO0u0+dkTW7zCaY7ds65CcmvZ/ahKz2ES3Tk6TNks1MJSyaQ9rFLs8AqA==",
"peer": true,
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/system-rsc": "2.3.21",
@@ -2424,6 +2428,7 @@
"version": "2.4.25",
"resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.25.tgz",
"integrity": "sha512-nTptYhO1V9rMoh9SJDnMfaSmFuoXvbem1UuwgHcraRtqy/TIVBPqv26JEGzSoUCL194TDGOJpqrpMuab/PdXcw==",
"peer": true,
"dependencies": {
"@heroui/shared-utils": "2.1.12",
"color": "^4.2.3",
@@ -5426,6 +5431,7 @@
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@@ -5884,6 +5890,7 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -6064,6 +6071,14 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/@types/prismjs": {
"version": "1.26.5",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
@@ -6084,6 +6099,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -6124,6 +6140,7 @@
"integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.18.0",
@@ -6181,6 +6198,7 @@
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.18.0",
@@ -6719,6 +6737,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7113,6 +7132,52 @@
"@babel/types": "^7.23.6"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/babel-plugin-macros/node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/babel-plugin-macros/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"license": "ISC",
"optional": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -7253,6 +7318,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -7941,7 +8007,8 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -8659,6 +8726,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -8782,6 +8850,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8862,6 +8931,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -8953,6 +9023,7 @@
"integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"aria-query": "^5.3.2",
"array-includes": "^3.1.8",
@@ -9047,6 +9118,7 @@
"integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"array-includes": "^3.1.8",
"array.prototype.findlast": "^1.2.5",
@@ -9080,6 +9152,7 @@
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -9347,6 +9420,7 @@
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -10341,6 +10415,7 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -11102,6 +11177,7 @@
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz",
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
"dev": true,
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@@ -12808,6 +12884,7 @@
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"peer": true,
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
@@ -12900,6 +12977,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@inquirer/confirm": "^5.0.0",
"@mswjs/interceptors": "^0.40.0",
@@ -13624,6 +13702,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -13701,6 +13780,7 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -13924,6 +14004,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13972,6 +14053,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -14084,6 +14166,7 @@
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
"integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
"peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -14445,6 +14528,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -15420,6 +15504,7 @@
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@@ -15546,6 +15631,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15847,6 +15933,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16151,6 +16238,7 @@
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -16321,6 +16409,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},

View File

@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
"node": ">=22.0.0"
"node": ">=22.12.0"
},
"dependencies": {
"@heroui/react": "2.8.7",
@@ -121,7 +121,7 @@
},
"packageManager": "npm@10.5.0",
"volta": {
"node": "22.0.0"
"node": "22.12.0"
},
"msw": {
"workerDirectory": [

View File

@@ -66,7 +66,7 @@ export function ChatInterface() {
const posthog = usePostHog();
const { setMessageToSend } = useConversationStore();
const { data: conversation } = useActiveConversation();
const { errorMessage } = useErrorMessageStore();
const { errorMessage, removeErrorMessage } = useErrorMessageStore();
const { isLoadingMessages } = useWsClient();
const { isTask, taskStatus, taskDetail } = useTaskPolling();
const conversationWebSocket = useConversationWebSocket();
@@ -342,7 +342,12 @@ export function ChatInterface() {
{!hitBottom && <ScrollToBottomButton onClick={scrollDomToBottom} />}
</div>
{errorMessage && <ErrorMessageBanner message={errorMessage} />}
{errorMessage && (
<ErrorMessageBanner
message={errorMessage}
onDismiss={removeErrorMessage}
/>
)}
<InteractiveChatBox onSubmit={handleSendMessage} />
</div>

View File

@@ -1,15 +1,45 @@
import { Trans } from "react-i18next";
import React from "react";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router";
import i18n from "#/i18n";
import { X } from "lucide-react";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
interface ErrorMessageBannerProps {
message: string;
onDismiss?: () => void;
}
export function ErrorMessageBanner({ message }: ErrorMessageBannerProps) {
const DEFAULT_MAX_COLLAPSED_CHARS = 220;
export function ErrorMessageBanner({
message,
onDismiss,
}: ErrorMessageBannerProps) {
const { t, i18n } = useTranslation();
const [isExpanded, setIsExpanded] = React.useState(false);
const isI18nKey = i18n.exists(message);
const displayTextForLength = isI18nKey ? String(t(message)) : message;
const shouldShowToggle =
displayTextForLength.length > DEFAULT_MAX_COLLAPSED_CHARS;
const isCollapsed = shouldShowToggle && !isExpanded;
return (
<div className="w-full rounded-lg p-2 text-black border border-red-800 bg-red-500">
{i18n.exists(message) ? (
<div
className="w-full rounded-lg p-2 border border-[#FF0006] bg-[#4A0709] flex gap-2 items-start text-white"
data-testid="error-message-banner"
>
<div className="min-w-0 flex-1">
<div
className={cn(
"whitespace-pre-wrap wrap-break-words",
isCollapsed && "line-clamp-3",
)}
data-testid="error-message-banner-content"
>
{isI18nKey ? (
<Trans
i18nKey={message}
components={{
@@ -27,5 +57,32 @@ export function ErrorMessageBanner({ message }: ErrorMessageBannerProps) {
message
)}
</div>
{shouldShowToggle && (
<button
type="button"
className="mt-1 text-xs underline font-semibold cursor-pointer"
onClick={() => setIsExpanded((prev) => !prev)}
data-testid="error-message-banner-toggle"
>
{isExpanded
? t(I18nKey.COMMON$VIEW_LESS)
: t(I18nKey.COMMON$VIEW_MORE)}
</button>
)}
</div>
{onDismiss && (
<button
type="button"
onClick={onDismiss}
className="shrink-0 rounded-md p-1 hover:bg-black/10 cursor-pointer"
aria-label={t(I18nKey.BUTTON$CLOSE)}
data-testid="error-message-banner-dismiss"
>
<X className="h-4 w-4" />
</button>
)}
</div>
);
}

View File

@@ -43,7 +43,6 @@ export default defineConfig(({ mode }) => {
"i18next-browser-languagedetector",
"react-i18next",
"axios",
"date-fns",
"@uidotdev/usehooks",
"react-icons/fa6",
"react-icons/fa",
@@ -51,8 +50,6 @@ export default defineConfig(({ mode }) => {
"tailwind-merge",
"@heroui/react",
"lucide-react",
"react-select",
"react-select/async",
"@microlink/react-json-view",
"socket.io-client",
// These are discovered when launching conversations: