diff --git a/frontend/README.md b/frontend/README.md index f11e38bca7..0ab14b8528 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -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 diff --git a/frontend/__tests__/components/chat/error-message-banner.test.tsx b/frontend/__tests__/components/chat/error-message-banner.test.tsx new file mode 100644 index 0000000000..f1ea8eaa17 --- /dev/null +++ b/frontend/__tests__/components/chat/error-message-banner.test.tsx @@ -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( + , + ); + + 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(); + + const toggle = screen.getByTestId("error-message-banner-toggle"); + expect(toggle).toHaveTextContent("COMMON$VIEW_MORE"); + + await user.click(toggle); + expect(toggle).toHaveTextContent("COMMON$VIEW_LESS"); + }); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 99887b5e20..b476f24e99 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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" }, diff --git a/frontend/package.json b/frontend/package.json index 30a60413c2..fa1ca4a09b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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": [ diff --git a/frontend/src/components/features/chat/chat-interface.tsx b/frontend/src/components/features/chat/chat-interface.tsx index d57b5acc3e..d1a431e2ed 100644 --- a/frontend/src/components/features/chat/chat-interface.tsx +++ b/frontend/src/components/features/chat/chat-interface.tsx @@ -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 && } - {errorMessage && } + {errorMessage && ( + + )} diff --git a/frontend/src/components/features/chat/error-message-banner.tsx b/frontend/src/components/features/chat/error-message-banner.tsx index 36876ab7e3..85086791d2 100644 --- a/frontend/src/components/features/chat/error-message-banner.tsx +++ b/frontend/src/components/features/chat/error-message-banner.tsx @@ -1,30 +1,87 @@ -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 ( -
- {i18n.exists(message) ? ( - - link - - ), - }} - /> - ) : ( - message +
+
+
+ {isI18nKey ? ( + + link + + ), + }} + /> + ) : ( + message + )} +
+ + {shouldShowToggle && ( + + )} +
+ + {onDismiss && ( + )}
); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index e75428e6d6..4c721f9ff9 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -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: