From 269138eb951dbdd21ec443182a8b101e59f8547c Mon Sep 17 00:00:00 2001 From: Yezh1i Date: Wed, 22 Jul 2026 20:49:27 +0800 Subject: [PATCH 1/2] fix: animate chat scroll after send --- .../chat/components/sections/chat-area.tsx | 78 ++++++++++--- .../features/chat/model/chat-scroll.test.mjs | 110 ++++++++++++++++++ frontend/features/chat/model/chat-scroll.ts | 103 ++++++++++++++++ frontend/package.json | 2 +- 4 files changed, 276 insertions(+), 17 deletions(-) create mode 100644 frontend/features/chat/model/chat-scroll.test.mjs create mode 100644 frontend/features/chat/model/chat-scroll.ts diff --git a/frontend/features/chat/components/sections/chat-area.tsx b/frontend/features/chat/components/sections/chat-area.tsx index 53fa6025..95cd7fd3 100644 --- a/frontend/features/chat/components/sections/chat-area.tsx +++ b/frontend/features/chat/components/sections/chat-area.tsx @@ -12,6 +12,12 @@ import { ChatMessageBot, } from "@/features/chat/components/message/message-bot"; import { areChatAreaMessagesRenderEqual } from "@/features/chat/model/chat-message-render"; +import { + animateChatScrollToBottom, + resolveLiveAnchorMessageKey, + resolvePendingUserScrollKey, + schedulePendingUserScroll, +} from "@/features/chat/model/chat-scroll"; import { type AssistantReaction } from "@/features/chat/components/message/message-meta"; import type { ChatAreaMessage, MessageAttachment } from "@/features/chat/types/messages"; import { ChatMessageUser } from "@/features/chat/components/message/message-user"; @@ -43,6 +49,51 @@ import { AppLogo, DeeixLogo } from "@/shared/components/app-logo"; import { useBranding } from "@/shared/config/branding-provider"; import { PoweredByDeeix } from "@/shared/components/powered-by-deeix"; +function ScrollToPendingUser({ + scrollKey, + viewportRef, +}: { + scrollKey: string; + viewportRef: React.RefObject; +}) { + const handledScrollKeyRef = React.useRef(""); + + React.useLayoutEffect(() => { + if (!scrollKey) { + handledScrollKeyRef.current = ""; + return; + } + if (handledScrollKeyRef.current === scrollKey) { + return; + } + + handledScrollKeyRef.current = scrollKey; + let cancelAnimation = () => undefined; + const cancelSchedule = schedulePendingUserScroll( + (callback) => window.requestAnimationFrame(callback), + (frameID) => window.cancelAnimationFrame(frameID), + () => { + const viewport = viewportRef.current; + if (!viewport) { + return; + } + cancelAnimation = animateChatScrollToBottom( + viewport, + (callback) => window.requestAnimationFrame(callback), + (frameID) => window.cancelAnimationFrame(frameID), + ); + }, + ); + + return () => { + cancelSchedule(); + cancelAnimation(); + }; + }, [scrollKey, viewportRef]); + + return null; +} + function CompactDivider({ summaryPreview }: { summaryPreview: string }) { const t = useTranslations("chat.messages"); const [expanded, setExpanded] = React.useState(false); @@ -493,24 +544,15 @@ export function ChatArea({ } pruneScreenshotSelection?.(selectableMessagePublicIDs); }, [pruneScreenshotSelection, selectableMessagePublicIDs, selectionMode]); - const hasLiveMessage = React.useMemo( - () => messages.some((item) => item.isPending || item.isStreaming), + const messageViewportBoundaryRef = React.useRef(null); + const liveAnchorMessageKey = React.useMemo( + () => resolveLiveAnchorMessageKey(messages), + [messages], + ); + const pendingUserScrollKey = React.useMemo( + () => resolvePendingUserScrollKey(messages), [messages], ); - const messageViewportBoundaryRef = React.useRef(null); - const liveAnchorMessageKey = React.useMemo(() => { - if (!hasLiveMessage) { - return ""; - } - const liveMessageIndex = messages.findIndex((item) => item.isPending || item.isStreaming); - for (let index = liveMessageIndex - 1; index >= 0; index -= 1) { - const item = messages[index]; - if (item?.role === "user") { - return item.key; - } - } - return ""; - }, [hasLiveMessage, messages]); return ( <> @@ -570,6 +612,10 @@ export function ChatArea({
+ { + assert.equal( + resolveLiveAnchorMessageKey([ + { key: "previous-user", role: "user" }, + { key: "previous-assistant", role: "assistant" }, + { key: "pending-user", role: "user", isPending: true }, + { key: "pending-assistant", role: "assistant", isPending: true }, + ]), + "previous-user", + ); +}); + +test("animates submitted messages to the bottom over a controlled duration", () => { + assert.equal(chatScroll.CHAT_SEND_SCROLL_DURATION_MS, 700); + assert.equal(typeof chatScroll.animateChatScrollToBottom, "function"); + + const viewport = { scrollTop: 100, scrollHeight: 1100, clientHeight: 100 }; + const frames = []; + let nextFrameID = 0; + const cancelled = []; + const cancel = chatScroll.animateChatScrollToBottom( + viewport, + (callback) => { + frames.push(callback); + nextFrameID += 1; + return nextFrameID; + }, + (frameID) => cancelled.push(frameID), + ); + + frames.shift()(0); + assert.equal(viewport.scrollTop, 100); + frames.shift()(350); + assert.equal(viewport.scrollTop, 550); + frames.shift()(700); + assert.equal(viewport.scrollTop, 1000); + + cancel(); + assert.deepEqual(cancelled, [3]); +}); + +test("starts smooth scrolling after pending-message layout observers settle", () => { + assert.equal(typeof chatScroll.schedulePendingUserScroll, "function"); + + const frames = []; + const cancelled = []; + let nextFrameID = 0; + let scrollCount = 0; + const cancel = chatScroll.schedulePendingUserScroll( + (callback) => { + frames.push(callback); + nextFrameID += 1; + return nextFrameID; + }, + (frameID) => cancelled.push(frameID), + () => { + scrollCount += 1; + }, + ); + + assert.equal(scrollCount, 0); + frames.shift()(0); + assert.equal(scrollCount, 0); + frames.shift()(16); + assert.equal(scrollCount, 1); + + cancel(); + assert.deepEqual(cancelled, [1, 2]); +}); + +test("uses a newly pending user turn as the explicit scroll-to-bottom trigger", () => { + assert.equal(typeof chatScroll.resolvePendingUserScrollKey, "function"); + assert.equal( + chatScroll.resolvePendingUserScrollKey([ + { key: "previous-user", role: "user" }, + { key: "previous-assistant", role: "assistant" }, + { key: "pending-user", role: "user", isPending: true }, + { key: "pending-assistant", role: "assistant", isPending: true }, + ]), + "pending-user", + ); +}); + +test("anchors an assistant-only live run to its parent user message", () => { + assert.equal( + resolveLiveAnchorMessageKey([ + { key: "parent-user", role: "user" }, + { key: "pending-assistant", role: "assistant", isStreaming: true }, + ]), + "parent-user", + ); +}); + +test("returns no live anchor or submit trigger when the conversation has no live message", () => { + const messages = [ + { key: "user", role: "user" }, + { key: "assistant", role: "assistant" }, + ]; + + assert.equal(resolveLiveAnchorMessageKey(messages), ""); + assert.equal(typeof chatScroll.resolvePendingUserScrollKey, "function"); + assert.equal(chatScroll.resolvePendingUserScrollKey(messages), ""); +}); diff --git a/frontend/features/chat/model/chat-scroll.ts b/frontend/features/chat/model/chat-scroll.ts new file mode 100644 index 00000000..3c97cb2a --- /dev/null +++ b/frontend/features/chat/model/chat-scroll.ts @@ -0,0 +1,103 @@ +import type { ChatAreaMessage } from "@/features/chat/types/messages"; + +type ScrollAnchorMessage = Pick; + +export const CHAT_SEND_SCROLL_DURATION_MS = 700; + +type RequestAnimationFrame = (callback: (timestamp: number) => void) => number; +type CancelAnimationFrame = (frameID: number) => void; +type ChatScrollViewport = Pick; + +function easeInOutCubic(progress: number) { + return progress < 0.5 + ? 4 * progress * progress * progress + : 1 - (-2 * progress + 2) ** 3 / 2; +} + +export function animateChatScrollToBottom( + viewport: ChatScrollViewport, + requestFrame: RequestAnimationFrame, + cancelFrame: CancelAnimationFrame, + durationMS = CHAT_SEND_SCROLL_DURATION_MS, +) { + const startTop = viewport.scrollTop; + const initialTargetTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight); + if (initialTargetTop <= startTop) { + viewport.scrollTop = initialTargetTop; + return () => undefined; + } + + let animationFrameID = 0; + let startTimestamp: number | null = null; + let cancelled = false; + const step = (timestamp: number) => { + if (cancelled) { + return; + } + if (startTimestamp === null) { + startTimestamp = timestamp; + } + + const progress = Math.min(1, Math.max(0, (timestamp - startTimestamp) / durationMS)); + const targetTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight); + viewport.scrollTop = startTop + (targetTop - startTop) * easeInOutCubic(progress); + if (progress < 1) { + animationFrameID = requestFrame(step); + } + }; + + animationFrameID = requestFrame(step); + return () => { + cancelled = true; + if (animationFrameID > 0) { + cancelFrame(animationFrameID); + } + }; +} + +export function schedulePendingUserScroll( + requestFrame: RequestAnimationFrame, + cancelFrame: CancelAnimationFrame, + scroll: () => void, +) { + let secondFrameID: number | null = null; + const firstFrameID = requestFrame(() => { + secondFrameID = requestFrame(() => { + scroll(); + }); + }); + + return () => { + cancelFrame(firstFrameID); + if (secondFrameID !== null) { + cancelFrame(secondFrameID); + } + }; +} + +export function resolveLiveAnchorMessageKey(messages: ScrollAnchorMessage[]) { + const liveMessageIndex = messages.findIndex((item) => item.isPending || item.isStreaming); + if (liveMessageIndex < 0) { + return ""; + } + + for (let index = liveMessageIndex - 1; index >= 0; index -= 1) { + const item = messages[index]; + if (item?.role === "user") { + return item.key; + } + } + + return ""; +} + +export function resolvePendingUserScrollKey(messages: ScrollAnchorMessage[]) { + for (let index = messages.length - 1; index >= 0; index -= 1) { + const item = messages[index]; + if (item?.role === "user" && item.isPending) { + return item.key; + } + } + + return ""; +} diff --git a/frontend/package.json b/frontend/package.json index f62f19de..d04c7cea 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "analyze:output": "next experimental-analyze --output", "start": "next start", "typecheck": "tsc --noEmit --pretty false", - "test": "node --no-warnings --test features/chat/components/sections/chat-model-picker-layout.test.mjs", + "test": "node --no-warnings --test features/chat/components/sections/chat-model-picker-layout.test.mjs features/chat/model/chat-scroll.test.mjs", "check": "pnpm lint && pnpm typecheck", "lint": "biome lint .", "lint:fix": "biome lint --write .", From f66e61fb75f56094add8b10004d91c094ff0a689 Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:39:15 +0800 Subject: [PATCH 2/2] fix: smooth chat scroll after sending a message --- .../chat/components/sections/chat-area.tsx | 74 ++++++------ .../chat-model-picker-layout.test.mjs | 75 ------------ .../features/chat/model/chat-scroll.test.mjs | 110 ------------------ frontend/features/chat/model/chat-scroll.ts | 103 ---------------- frontend/package.json | 1 - 5 files changed, 33 insertions(+), 330 deletions(-) delete mode 100644 frontend/features/chat/components/sections/chat-model-picker-layout.test.mjs delete mode 100644 frontend/features/chat/model/chat-scroll.test.mjs delete mode 100644 frontend/features/chat/model/chat-scroll.ts diff --git a/frontend/features/chat/components/sections/chat-area.tsx b/frontend/features/chat/components/sections/chat-area.tsx index 95cd7fd3..f3566d41 100644 --- a/frontend/features/chat/components/sections/chat-area.tsx +++ b/frontend/features/chat/components/sections/chat-area.tsx @@ -12,12 +12,6 @@ import { ChatMessageBot, } from "@/features/chat/components/message/message-bot"; import { areChatAreaMessagesRenderEqual } from "@/features/chat/model/chat-message-render"; -import { - animateChatScrollToBottom, - resolveLiveAnchorMessageKey, - resolvePendingUserScrollKey, - schedulePendingUserScroll, -} from "@/features/chat/model/chat-scroll"; import { type AssistantReaction } from "@/features/chat/components/message/message-meta"; import type { ChatAreaMessage, MessageAttachment } from "@/features/chat/types/messages"; import { ChatMessageUser } from "@/features/chat/components/message/message-user"; @@ -39,6 +33,7 @@ import { MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, + useMessageScroller, } from "@/components/ui/message-scroller"; import { ChatMessagePositionRail, @@ -49,14 +44,9 @@ import { AppLogo, DeeixLogo } from "@/shared/components/app-logo"; import { useBranding } from "@/shared/config/branding-provider"; import { PoweredByDeeix } from "@/shared/components/powered-by-deeix"; -function ScrollToPendingUser({ - scrollKey, - viewportRef, -}: { - scrollKey: string; - viewportRef: React.RefObject; -}) { +function ScrollToPendingUser({ scrollKey }: { scrollKey: string }) { const handledScrollKeyRef = React.useRef(""); + const { scrollToEnd } = useMessageScroller(); React.useLayoutEffect(() => { if (!scrollKey) { @@ -68,28 +58,20 @@ function ScrollToPendingUser({ } handledScrollKeyRef.current = scrollKey; - let cancelAnimation = () => undefined; - const cancelSchedule = schedulePendingUserScroll( - (callback) => window.requestAnimationFrame(callback), - (frameID) => window.cancelAnimationFrame(frameID), - () => { - const viewport = viewportRef.current; - if (!viewport) { - return; - } - cancelAnimation = animateChatScrollToBottom( - viewport, - (callback) => window.requestAnimationFrame(callback), - (frameID) => window.cancelAnimationFrame(frameID), - ); - }, - ); - + let secondFrameID: number | null = null; + const firstFrameID = window.requestAnimationFrame(() => { + secondFrameID = window.requestAnimationFrame(() => { + const reducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false; + scrollToEnd({ behavior: reducedMotion ? "auto" : "smooth" }); + }); + }); return () => { - cancelSchedule(); - cancelAnimation(); + window.cancelAnimationFrame(firstFrameID); + if (secondFrameID !== null) { + window.cancelAnimationFrame(secondFrameID); + } }; - }, [scrollKey, viewportRef]); + }, [scrollKey, scrollToEnd]); return null; } @@ -544,13 +526,26 @@ export function ChatArea({ } pruneScreenshotSelection?.(selectableMessagePublicIDs); }, [pruneScreenshotSelection, selectableMessagePublicIDs, selectionMode]); - const messageViewportBoundaryRef = React.useRef(null); - const liveAnchorMessageKey = React.useMemo( - () => resolveLiveAnchorMessageKey(messages), + const hasLiveMessage = React.useMemo( + () => messages.some((item) => item.isPending || item.isStreaming), [messages], ); + const messageViewportBoundaryRef = React.useRef(null); + const liveAnchorMessageKey = React.useMemo(() => { + if (!hasLiveMessage) { + return ""; + } + const liveMessageIndex = messages.findIndex((item) => item.isPending || item.isStreaming); + for (let index = liveMessageIndex - 1; index >= 0; index -= 1) { + const item = messages[index]; + if (item?.role === "user") { + return item.key; + } + } + return ""; + }, [hasLiveMessage, messages]); const pendingUserScrollKey = React.useMemo( - () => resolvePendingUserScrollKey(messages), + () => [...messages].reverse().find((item) => item.role === "user" && item.isPending)?.key ?? "", [messages], ); @@ -612,10 +607,7 @@ export function ChatArea({
- + { - assert.equal( - resolveDesktopModelMenuListMaxHeight({ - viewportTop: 24, - viewportBottom: 144, - sideOffset: 8, - verticalChrome: 40, - }), - 80, - ); -}); - -test("uses all normal viewport space remaining after panel chrome", () => { - assert.equal( - resolveDesktopModelMenuListMaxHeight({ - viewportTop: 24, - viewportBottom: 424, - sideOffset: 8, - verticalChrome: 40, - }), - 360, - ); -}); - -test("limits a positioned submenu to its remaining viewport space", () => { - assert.equal(resolveDesktopMenuListMaxHeight(72, 12), 60); -}); - -test("uses the larger space below the trigger and deducts the side offset", () => { - assert.equal( - resolveDesktopModelMenuListMaxHeight({ - viewportTop: 24, - viewportBottom: 424, - triggerTop: 96, - triggerBottom: 124, - sideOffset: 8, - verticalChrome: 40, - }), - 252, - ); -}); - -test("uses the larger space above the trigger", () => { - assert.equal( - resolveDesktopModelMenuListMaxHeight({ - viewportTop: 24, - viewportBottom: 424, - triggerTop: 324, - triggerBottom: 352, - sideOffset: 8, - verticalChrome: 40, - }), - 252, - ); -}); - -test("returns zero when panel chrome consumes all available space", () => { - assert.equal( - resolveDesktopModelMenuListMaxHeight({ - viewportTop: 24, - viewportBottom: 64, - sideOffset: 8, - verticalChrome: 40, - }), - 0, - ); -}); diff --git a/frontend/features/chat/model/chat-scroll.test.mjs b/frontend/features/chat/model/chat-scroll.test.mjs deleted file mode 100644 index 8dab4ac5..00000000 --- a/frontend/features/chat/model/chat-scroll.test.mjs +++ /dev/null @@ -1,110 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import * as chatScroll from "./chat-scroll.ts"; - -const { resolveLiveAnchorMessageKey } = chatScroll; - -test("keeps the previous user as the stream resize anchor", () => { - assert.equal( - resolveLiveAnchorMessageKey([ - { key: "previous-user", role: "user" }, - { key: "previous-assistant", role: "assistant" }, - { key: "pending-user", role: "user", isPending: true }, - { key: "pending-assistant", role: "assistant", isPending: true }, - ]), - "previous-user", - ); -}); - -test("animates submitted messages to the bottom over a controlled duration", () => { - assert.equal(chatScroll.CHAT_SEND_SCROLL_DURATION_MS, 700); - assert.equal(typeof chatScroll.animateChatScrollToBottom, "function"); - - const viewport = { scrollTop: 100, scrollHeight: 1100, clientHeight: 100 }; - const frames = []; - let nextFrameID = 0; - const cancelled = []; - const cancel = chatScroll.animateChatScrollToBottom( - viewport, - (callback) => { - frames.push(callback); - nextFrameID += 1; - return nextFrameID; - }, - (frameID) => cancelled.push(frameID), - ); - - frames.shift()(0); - assert.equal(viewport.scrollTop, 100); - frames.shift()(350); - assert.equal(viewport.scrollTop, 550); - frames.shift()(700); - assert.equal(viewport.scrollTop, 1000); - - cancel(); - assert.deepEqual(cancelled, [3]); -}); - -test("starts smooth scrolling after pending-message layout observers settle", () => { - assert.equal(typeof chatScroll.schedulePendingUserScroll, "function"); - - const frames = []; - const cancelled = []; - let nextFrameID = 0; - let scrollCount = 0; - const cancel = chatScroll.schedulePendingUserScroll( - (callback) => { - frames.push(callback); - nextFrameID += 1; - return nextFrameID; - }, - (frameID) => cancelled.push(frameID), - () => { - scrollCount += 1; - }, - ); - - assert.equal(scrollCount, 0); - frames.shift()(0); - assert.equal(scrollCount, 0); - frames.shift()(16); - assert.equal(scrollCount, 1); - - cancel(); - assert.deepEqual(cancelled, [1, 2]); -}); - -test("uses a newly pending user turn as the explicit scroll-to-bottom trigger", () => { - assert.equal(typeof chatScroll.resolvePendingUserScrollKey, "function"); - assert.equal( - chatScroll.resolvePendingUserScrollKey([ - { key: "previous-user", role: "user" }, - { key: "previous-assistant", role: "assistant" }, - { key: "pending-user", role: "user", isPending: true }, - { key: "pending-assistant", role: "assistant", isPending: true }, - ]), - "pending-user", - ); -}); - -test("anchors an assistant-only live run to its parent user message", () => { - assert.equal( - resolveLiveAnchorMessageKey([ - { key: "parent-user", role: "user" }, - { key: "pending-assistant", role: "assistant", isStreaming: true }, - ]), - "parent-user", - ); -}); - -test("returns no live anchor or submit trigger when the conversation has no live message", () => { - const messages = [ - { key: "user", role: "user" }, - { key: "assistant", role: "assistant" }, - ]; - - assert.equal(resolveLiveAnchorMessageKey(messages), ""); - assert.equal(typeof chatScroll.resolvePendingUserScrollKey, "function"); - assert.equal(chatScroll.resolvePendingUserScrollKey(messages), ""); -}); diff --git a/frontend/features/chat/model/chat-scroll.ts b/frontend/features/chat/model/chat-scroll.ts deleted file mode 100644 index 3c97cb2a..00000000 --- a/frontend/features/chat/model/chat-scroll.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ChatAreaMessage } from "@/features/chat/types/messages"; - -type ScrollAnchorMessage = Pick; - -export const CHAT_SEND_SCROLL_DURATION_MS = 700; - -type RequestAnimationFrame = (callback: (timestamp: number) => void) => number; -type CancelAnimationFrame = (frameID: number) => void; -type ChatScrollViewport = Pick; - -function easeInOutCubic(progress: number) { - return progress < 0.5 - ? 4 * progress * progress * progress - : 1 - (-2 * progress + 2) ** 3 / 2; -} - -export function animateChatScrollToBottom( - viewport: ChatScrollViewport, - requestFrame: RequestAnimationFrame, - cancelFrame: CancelAnimationFrame, - durationMS = CHAT_SEND_SCROLL_DURATION_MS, -) { - const startTop = viewport.scrollTop; - const initialTargetTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight); - if (initialTargetTop <= startTop) { - viewport.scrollTop = initialTargetTop; - return () => undefined; - } - - let animationFrameID = 0; - let startTimestamp: number | null = null; - let cancelled = false; - const step = (timestamp: number) => { - if (cancelled) { - return; - } - if (startTimestamp === null) { - startTimestamp = timestamp; - } - - const progress = Math.min(1, Math.max(0, (timestamp - startTimestamp) / durationMS)); - const targetTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight); - viewport.scrollTop = startTop + (targetTop - startTop) * easeInOutCubic(progress); - if (progress < 1) { - animationFrameID = requestFrame(step); - } - }; - - animationFrameID = requestFrame(step); - return () => { - cancelled = true; - if (animationFrameID > 0) { - cancelFrame(animationFrameID); - } - }; -} - -export function schedulePendingUserScroll( - requestFrame: RequestAnimationFrame, - cancelFrame: CancelAnimationFrame, - scroll: () => void, -) { - let secondFrameID: number | null = null; - const firstFrameID = requestFrame(() => { - secondFrameID = requestFrame(() => { - scroll(); - }); - }); - - return () => { - cancelFrame(firstFrameID); - if (secondFrameID !== null) { - cancelFrame(secondFrameID); - } - }; -} - -export function resolveLiveAnchorMessageKey(messages: ScrollAnchorMessage[]) { - const liveMessageIndex = messages.findIndex((item) => item.isPending || item.isStreaming); - if (liveMessageIndex < 0) { - return ""; - } - - for (let index = liveMessageIndex - 1; index >= 0; index -= 1) { - const item = messages[index]; - if (item?.role === "user") { - return item.key; - } - } - - return ""; -} - -export function resolvePendingUserScrollKey(messages: ScrollAnchorMessage[]) { - for (let index = messages.length - 1; index >= 0; index -= 1) { - const item = messages[index]; - if (item?.role === "user" && item.isPending) { - return item.key; - } - } - - return ""; -} diff --git a/frontend/package.json b/frontend/package.json index d04c7cea..d75894a6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,6 @@ "analyze:output": "next experimental-analyze --output", "start": "next start", "typecheck": "tsc --noEmit --pretty false", - "test": "node --no-warnings --test features/chat/components/sections/chat-model-picker-layout.test.mjs features/chat/model/chat-scroll.test.mjs", "check": "pnpm lint && pnpm typecheck", "lint": "biome lint .", "lint:fix": "biome lint --write .",