Fix a crash on the filtered agent routes, and two animation defects

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.
This commit is contained in:
Alex committed 2026-09-22 00:24:52 +01:00
1 parent ac87430715
commit 2f6b2ee8dd
2 files changed
+33 -13

No files matched your search

+13 -9
View File
@@ -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 (
<div className="mt-8 flex flex-col gap-4">
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
+20 -4
View File
@@ -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);