From 2f6b2ee8dd059c04f95f7cd6089098f3135a398e Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 22 Sep 2026 00:24:52 +0100 Subject: [PATCH] Fix a crash on the filtered agent routes, and two animation defects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentSection computed its folder breadcrumb in a useMemo placed below two early returns for the empty states. A hook after an early return is skipped on the render that takes it, which React rejects outright with "rendered fewer hooks than expected" — the page showed only the error boundary. Reachable now that each filter is its own route: landing straight on an empty one renders once while the data loads and again once it arrives empty, taking the early return the second time. The memo moves above them. Two problems in the chat's entrance animations, found while looking for the reported flicker: - .fade-in-bubble carried `opacity: 0` on the element and reached full opacity only by running `fadeInUp` to completion with `forwards`. The answer text was therefore visible *because* an animation had finished, so anything that stopped one running left it blank — including the obvious reduced-motion reset, which is presumably why the rule covers only .shimmer-text today. The start state moves into the keyframes, the element rests visible, and both entrances now honour prefers-reduced-motion. - The timings were long for their jobs. An expanded tool call reaches its full height at once, so fading its content over half a second read as the content lagging the layout rather than as a reveal; it is now 0.16s. The answer entrance goes to 0.26s with a 6px rise instead of 0.5s and 10px. --- frontend/src/agents/AgentsList.tsx | 22 +++++++++++++--------- frontend/src/index.css | 24 ++++++++++++++++++++---- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/frontend/src/agents/AgentsList.tsx b/frontend/src/agents/AgentsList.tsx index 4288ec21..079d7a56 100644 --- a/frontend/src/agents/AgentsList.tsx +++ b/frontend/src/agents/AgentsList.tsx @@ -400,6 +400,19 @@ function AgentSection({ onCreateFolder(name, currentFolderId || undefined); }; + // Must stay above the empty-state returns below: a hook after an early + // return is skipped on the render that takes it, which React rejects with + // "rendered fewer hooks than expected". Reachable now that each filter is + // its own route — landing straight on an empty one renders once while the + // data loads, then again once it arrives empty. + const breadcrumbItems = useMemo(() => { + if (!folders || folderPath.length === 0) return []; + return folderPath.map((folderId) => { + const folder = folders.find((f) => f.id === folderId); + return { id: folderId, name: folder?.name || '' }; + }); + }, [folders, folderPath]); + const hasNoAgentsAtAll = !isLoading && totalAgents === 0; const isSearchingWithNoResults = !isLoading && searchQuery && filteredAgents.length === 0 && totalAgents > 0; @@ -433,15 +446,6 @@ function AgentSection({ ); } - // Build breadcrumb items from folder path - const breadcrumbItems = useMemo(() => { - if (!folders || folderPath.length === 0) return []; - return folderPath.map((folderId) => { - const folder = folders.find((f) => f.id === folderId); - return { id: folderId, name: folder?.name || '' }; - }); - }, [folders, folderPath]); - return (
diff --git a/frontend/src/index.css b/frontend/src/index.css index d3bd0ffc..a9e6438d 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -690,8 +690,11 @@ Avoid over-scrolling in mobile browsers font-family: var(--font-mono); } + /* Disclosure content, e.g. an expanded tool call. Short on purpose: the + panel's height appears at once, so a long fade reads as the content + lagging behind the layout rather than as a reveal. */ .fade-in { - animation: fadeIn 0.5s ease-in-out; + animation: fadeIn 0.16s ease-out; } @keyframes fadeIn { @@ -703,10 +706,13 @@ Avoid over-scrolling in mobile browsers } } + /* The start state lives in the keyframes, not here: with `opacity: 0` on + the element itself the content was only ever visible *because* an + animation had run to completion, so anything that stopped it running — + a cancelled animation, a reduced-motion reset — left the answer blank. + The element now rests visible and the animation is purely additive. */ .fade-in-bubble { - opacity: 0; - transform: translateY(10px); - animation: fadeInUp 0.5s forwards; + animation: fadeInUp 0.26s ease-out; } .shimmer-text { @@ -754,9 +760,19 @@ Avoid over-scrolling in mobile browsers .dark .shimmer-text { background-image: none; } + + /* Safe to drop outright now that both rest in their final state. */ + .fade-in, + .fade-in-bubble { + animation: none; + } } @keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(6px); + } to { opacity: 1; transform: translateY(0);